Skip to content

feat(billing): dedicated Credit usage page with date-range filter and CSV export#5405

Merged
waleedlatif1 merged 17 commits into
stagingfrom
feat/credit-usage-page
Jul 4, 2026
Merged

feat(billing): dedicated Credit usage page with date-range filter and CSV export#5405
waleedlatif1 merged 17 commits into
stagingfrom
feat/credit-usage-page

Conversation

@waleedlatif1

@waleedlatif1 waleedlatif1 commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Moved Credit Usage out of the inline Billing section into its own page (/settings/billing/credit-usage), reached via a "View usage logs" link — mirrors the secrets/[credentialId] detail-route pattern (own back button, own chrome).
  • Billing settings now shows a compact glance (30-day total + link) instead of the full inline list.
  • New page has day-level date presets (Today / Last 7 days / Last 30 days / All time) plus a working Custom range picker.
  • Fixed the broken Custom range picker in the existing Audit Logs page — the trigger was a modal ChipSelect (Radix DropdownMenu, modal by default); selecting "Custom range" opened the Calendar popover in the same tick the modal menu began its close/focus-lock teardown, trapping the popover non-interactive. Swapped to the non-modal ChipCombobox, matching the pattern the main Logs page already uses successfully. Also trimmed the Audit Logs preset list from 11 to 8 so the menu fits without scrolling.
  • Redesigned rows: dropped the raw model description + badge, now show the humanized source ("Chat", "Agent block", etc.) or Workflow: <name> for workflow-sourced events.
  • Added CSV export of the currently-filtered logs via a new GET /api/users/me/usage-logs/export route (synchronous, single-response — a user's own credit ledger is bounded, unlike a workspace-wide export).
  • Ran a 4-angle /simplify pass after the initial build: moved workflow-name enrichment into getUserUsageLogs itself (matching the established list-logs.ts join pattern) instead of a route-layer helper duplicated across two routes, which also eliminated a redundant SUM/GROUP BY aggregate being recomputed on every page of a paginated export; fixed an off-by-one in the export's pagination loop; deduplicated the source-label map; reused the existing CSV escaping helper instead of hand-rolling one; added a modal prop to the shared ChipSelect component so future call sites don't have to rediscover the same trap.

Enterprise gating preserved — the Credit usage section/page is not shown to Enterprise members/owners (unchanged from #5391).

Type of Change

  • New feature
  • Bug fix (Audit Logs Custom range)

Testing

  • bunx tsc --noEmit, biome, and bun run check:api-validation all pass.
  • 41 vitest tests pass across the touched files (usage-logs list/export routes, credit conversion, usage-log core).
  • Verified live end-to-end in a real browser: page navigation (back link), Custom range picker opening/applying on both the new page and Audit Logs, day presets, row rendering (source labels, workflow name resolution, no badge), CSV export (downloaded file matches on-screen rows and reconciles with the displayed total), compact Billing summary + link.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…total

Cursor Bugbot (medium): each row rounded its own dollar cost to
credits independently while the header total rounded the summed
dollars once — over enough rows those two roundings can visibly
disagree, the exact "line items don't add up to the total" class of
bug apportionCredits was already built to prevent (used by the trace
view / cost breakdown). Route now apportions each page's row credits
against that page's dollar sum instead of rounding rows independently.

Added a test with three sub-cent rows that would each independently
round to 0 credits (but sum to 1) to prove the reconciliation holds.
Cursor Bugbot (medium): keepPreviousData kept the prior period's rows
and total on screen while a newly selected period fetched, but the
dropdown label updated immediately — so during the transition the
displayed numbers were labeled under a period they didn't belong to.
Now reads isPlaceholderData (the standard TanStack Query signal for
"this data is a stale placeholder, not a fresh fetch for the current
key") and dims the list while it's true, matching the same flag
already used for this exact purpose in integration-skills-section.tsx.
Cursor Bugbot (low): with apportioned per-row credits, a row with a
real but sub-credit dollarCost can legitimately apportion to 0 credits
once a sibling row absorbs the shared rounding remainder — rendering a
flat "0 credits" reads as if nothing was charged, inconsistent with
formatCreditCost's "<1 credit" wording used elsewhere in billing.

Added dollarCost to the wire response (needed to distinguish a
genuinely free row from a rounded-to-zero one) and a small
formatRowCredits helper that only changes the label, not the
underlying creditCost number, so the page-total reconciliation from
the prior fix is unaffected.
Custom range silently did nothing: the time-range trigger was a
ChipSelect (Radix DropdownMenu, modal by default), and selecting
"Custom range" opened the Calendar popover in the same tick the modal
menu began its close/focus-lock cleanup, trapping the popover
non-interactive. Swapped to ChipCombobox (Radix Popover, non-modal),
mirroring the already-working pattern in the main Logs page exactly.

Also trimmed the preset list from 11 to 8 entries (dropped Past 30
minutes/12 hours/14 days) so the menu fits without scrolling.
… CSV export

Follow-up to #5391 per team feedback in Slack: move the credit usage
list out of the inline Billing section into its own page, redesign
rows to show source ("Chat", "Workflow: <name>") instead of a raw
model description + badge, and add real date-range filtering and
export.

- Billing settings now shows a compact glance (30-day total + a "View
  usage logs" link) instead of the full inline list.
- New /settings/billing/credit-usage page (sibling of [section],
  mirrors the secrets/[credentialId] detail-route pattern) with day
  presets (Today/7d/30d/All time) plus a working Custom range picker
  — the same ChipCombobox+Popover+Calendar wiring the audit-logs fix
  in this branch uses, not the broken ChipSelect pattern.
- Rows show the humanized source label, or "Workflow: <name>" for
  workflow-sourced events (new server-side workflow-name lookup,
  batched per page). Dropped the redundant badge and raw model
  description.
- CSV export of the currently-filtered logs via a new GET
  .../usage-logs/export route (mode: 'text' contract, synchronous
  single-response CSV — the dataset is a bounded per-user ledger, not
  a workspace-wide export, so no async job queue needed). Query-filter
  logic (date-range resolution, workflow-name lookup) is shared with
  the list route via shared.ts rather than duplicated.
- period/startDate/endDate live in the URL via a co-located
  search-params.ts; the list query keeps keepPreviousData +
  isPlaceholderData dimming during filter transitions, matching the
  behavior already shipped in #5391.

Verified live end-to-end: back link navigation, custom range picker
opens and applies, day presets, CSV export downloads and matches the
on-screen rows exactly (credits reconcile with the total), compact
Billing summary + link.
…s, dedup helpers

/simplify pass over the credit-usage-page branch (4 parallel review
angles: reuse, simplification, efficiency, altitude):

- getUserUsageLogs now LEFT JOINs workflow and returns workflowName
  directly (matching lib/logs/list-logs.ts's established pattern),
  eliminating the route-layer resolveWorkflowNames query that both the
  list and export routes previously ran independently.
- Added includeSummary (default true) to getUserUsageLogs so the
  export route's cursor loop can skip the cursor-independent
  SUM/GROUP BY aggregate it never reads — that aggregate was being
  recomputed on every page of a paginated export for no reason.
- Fixed an off-by-one in the export's pagination loop: `<=
  MAX_EXPORT_ROWS` let it fetch one more full page past the cap only
  to discard it; `< MAX_EXPORT_ROWS` with a shrinking per-page limit
  never overshoots.
- Deduplicated the SOURCE_LABELS map (was defined identically in both
  the page and the export route) into a shared, DB-free
  source-labels.ts both can import.
- Export route now builds CSV rows via lib/table/export-format.ts's
  toCsvRow/formatCsvValue instead of a hand-rolled escaper.
- Added formatApportionedCreditCost to conversion.ts so the page's row
  rendering shares its zero/sub-credit wording with formatCreditCost
  instead of re-deriving the same three-way branch.
- Replaced the generic requireStartDateForCustomPeriod<Schema> contract
  helper (nontrivial generic bound for a single four-line refine used
  at two call sites) with a plain shared error-options object.
- Removed the credit-usage page's dateRangeAppliedRef guard — a
  controlled Radix Popover never re-invokes onOpenChange in response
  to the parent's own setState call, so the guard was defending
  against a re-entrant close that can't happen.
- Added a modal prop to ChipSelect (forwarded to the underlying
  DropdownMenu, which already supported it) so a future call site that
  hits the same "modal select traps a same-tick Popover" bug the
  audit-logs Custom range fix worked around has a real fix available
  instead of having to swap components again.

Re-verified live end-to-end after the refactor: workflow-name
resolution, credit reconciliation, and CSV export all still correct.
@vercel

vercel Bot commented Jul 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 4, 2026 1:59am

Request Review

@cursor

cursor Bot commented Jul 3, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches billing/usage-log read paths and new authenticated export endpoints that can return large CSV payloads; credit apportionment logic changes how row totals are computed across UI and export.

Overview
Credit usage moves off the Billing settings panel into its own route (/settings/billing/credit-usage) with presets, a custom date range, infinite scroll, and CSV export via GET /api/users/me/usage-logs/export. Billing now shows only a 30-day total and a “View usage logs” link; Enterprise users are redirected server-side if they hit the page directly.

The usage-logs API gains custom period, shared resolveDateRange, workflow names on rows, and apportioned per-row credits (getUsageCreditsByLogId) so list, summary, and export stay consistent; optional includeCredits=false avoids a full-filter scan for the billing glance.

Audit logs get a matching CSV export (GET /api/audit-logs/export), an Export action in the UI, and a fix for Custom range by switching the time filter from modal ChipSelect to ChipCombobox (same pattern as Credit usage). Shared ChipSelect now accepts a modal prop for similar overlay cases.

Reviewed by Cursor Bugbot for commit 17a5a73. Configure here.

@greptile-apps

greptile-apps Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR moves Credit Usage out of the inline Billing section into a dedicated /settings/billing/credit-usage page with date-range presets, a custom date picker, and CSV export. It also fixes the broken Custom range picker in Audit Logs (swaps the modal ChipSelect for the non-modal ChipCombobox), redesigns row labels to humanize source names, and adds an audit-log CSV export.

  • New Credit Usage page: dedicated route with enterprise gate (server-side redirect), date presets, custom range picker, and infinite-scroll log list. Row credits are apportioned over the full filtered set (not per-page) so totals are always self-consistent.
  • CSV export for both Credit Usage and Audit Logs: paginated fetch loop with a safety-cap circuit-breaker; truncation is surfaced via X-Export-Truncated response header and a client-side toast.
  • Bug fix – Audit Logs Custom range: replaced modal ChipSelect (Radix DropdownMenu) with non-modal ChipCombobox (Radix Popover), eliminating the focus-lock race that silently swallowed calendar opens; also adds modal prop to ChipSelect for future call sites.

Confidence Score: 5/5

Safe to merge; all previously identified correctness issues have been fixed, and the new code is well-tested with 41 passing tests.

The enterprise redirect is now enforced server-side before any data renders. The startDate/endDate export-URL bug and the resolveDateRange cast have both been corrected. The Custom range focus-lock regression in Audit Logs is fixed by swapping to the non-modal combobox. No new correctness issues were found — the credit apportionment design (full-filter scan on every list request) is intentional and documented.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/app/api/users/me/usage-logs/export/route.ts New CSV export endpoint: pagination loop with safety-cap, workflow-name enrichment, and X-Export-Truncated header. Auth and contract validation are correctly applied; truncation flag logic is sound.
apps/sim/lib/billing/core/usage-log.ts Adds getUsageCreditsByLogId (full-filter credit apportionment), workflow LEFT JOIN for name enrichment, and includeSummary/cursorCreatedAt options; pagination cursor logic correctly migrated to Drizzle OR predicate.
apps/sim/app/workspace/[workspaceId]/settings/billing/credit-usage/page.tsx Server component with enterprise gate (getHighestPrioritySubscription + redirect) running before any Suspense/data-fetching; Suspense boundary wraps the nuqs-consuming CreditUsageView correctly.
apps/sim/app/workspace/[workspaceId]/settings/billing/credit-usage/credit-usage-view.tsx Client component for the Credit Usage page: period/date filters via nuqs, infinite-scroll list, and fetch-based CSV export that reads X-Export-Truncated before triggering the download. Previously reported startDate/endDate bug is fixed (each date is set independently).
apps/sim/ee/audit-logs/components/audit-logs.tsx Swaps modal ChipSelect for non-modal ChipCombobox to fix the Custom range focus-lock bug; adds CSV export via handleExportCsv; trims preset list from 11 to 8 options.
apps/sim/lib/api/contracts/user.ts Extends usageLogPeriodSchema with 'custom', adds parseableDateSchema + startDate/endDate fields, splits into usageLogsFilterSchema shared by list and export schemas, and exports new UsageLogEntry fields (workflowName, dollarCost).
apps/sim/hooks/queries/usage-logs.ts Adds startDate/endDate to the infinite-query key and filter, adds useUsageSummary for the compact Billing glance (skips apportionment via includeCredits=false), and introduces usageLogKeys.summary.
packages/emcn/src/components/chip-select/chip-select.tsx Adds optional modal prop forwarded to the underlying Radix DropdownMenu; non-breaking addition with good documentation on when to use it.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as CreditUsageView
    participant List as GET /usage-logs
    participant Export as GET /usage-logs/export
    participant Core as getUserUsageLogs
    participant Credits as getUsageCreditsByLogId
    participant DB as DB Replica

    UI->>List: "?period=30d&limit=25&includeCredits=true"
    List->>Core: "{ limit: 25, includeSummary: true }"
    Core->>DB: SELECT … LEFT JOIN workflow LIMIT 26
    Core->>DB: SELECT SUM(cost) GROUP BY source
    Core-->>List: "{ logs[25], summary, pagination }"
    List->>Credits: full filter (no limit)
    Credits->>DB: "SELECT id, cost WHERE userId & filter"
    Credits-->>List: "{ logId → credits }"
    List-->>UI: logs with creditCost + dollarCost + summary.totalCredits

    UI->>Export: "?period=30d (Download CSV button)"
    loop "until hasMore=false or safety cap"
        Export->>Core: "{ limit: 1000, includeSummary: false, cursor? }"
        Core->>DB: SELECT … LEFT JOIN workflow LIMIT 1001
        Core-->>Export: page of logs
    end
    Export->>Credits: full filter (no limit)
    Credits->>DB: "SELECT id, cost WHERE userId & filter"
    Credits-->>Export: "{ logId → credits }"
    Export-->>UI: CSV blob + X-Export-Truncated header
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as CreditUsageView
    participant List as GET /usage-logs
    participant Export as GET /usage-logs/export
    participant Core as getUserUsageLogs
    participant Credits as getUsageCreditsByLogId
    participant DB as DB Replica

    UI->>List: "?period=30d&limit=25&includeCredits=true"
    List->>Core: "{ limit: 25, includeSummary: true }"
    Core->>DB: SELECT … LEFT JOIN workflow LIMIT 26
    Core->>DB: SELECT SUM(cost) GROUP BY source
    Core-->>List: "{ logs[25], summary, pagination }"
    List->>Credits: full filter (no limit)
    Credits->>DB: "SELECT id, cost WHERE userId & filter"
    Credits-->>List: "{ logId → credits }"
    List-->>UI: logs with creditCost + dollarCost + summary.totalCredits

    UI->>Export: "?period=30d (Download CSV button)"
    loop "until hasMore=false or safety cap"
        Export->>Core: "{ limit: 1000, includeSummary: false, cursor? }"
        Core->>DB: SELECT … LEFT JOIN workflow LIMIT 1001
        Core-->>Export: page of logs
    end
    Export->>Credits: full filter (no limit)
    Credits->>DB: "SELECT id, cost WHERE userId & filter"
    Credits-->>Export: "{ logId → credits }"
    Export-->>UI: CSV blob + X-Export-Truncated header
Loading

Reviews (9): Last reviewed commit: "fix(billing): gate the credit-usage page..." | Re-trigger Greptile

Comment thread apps/sim/app/api/users/me/usage-logs/shared.ts
…ents

We only surface credits to the user, not the underlying dollar figure
— "Dollar cost" was the one place the export literally displayed a
dollar amount (the rest of the codebase uses dollarCost purely as an
internal signal to distinguish a sub-credit charge from a genuinely
free event, never rendered as a "$" value).
…ation

Greptile (P1) and Cursor Bugbot independently caught the same bug:
handleExport only forwarded startDate/endDate when BOTH were truthy,
but the list query and both API contracts treat endDate as optional
for a custom period (defaults to now). A user landing on a bookmarked
?period=custom&startDate=... URL would see populated rows and an
enabled Export button, then get a 400 on click since the export
omitted the required startDate too. Fixed by forwarding each date
independently, matching the list query's existing behavior.

Also addressed Greptile's other two findings:
- The export route now sets X-Export-Truncated so a 5,000-row-capped
  download is visible to the user (a toast), not just a server log.
  Reading that header meant switching the trigger from a plain anchor
  navigation to fetch+blob — an anchor can't inspect the response
  before the browser commits to the download.
- resolveDateRange now throws explicitly when a custom period is
  missing startDate instead of silencing the null check with `as
  string`, which would have produced a silent Invalid Date if ever
  called without prior contract validation.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/users/me/usage-logs/route.ts Outdated
…ination bug it exposed

A personal credit ledger doesn't have the same unbounded-growth problem
a workspace table does — capping the export at 5,000 rows just meant
long-tenured or high-usage accounts (exactly the ones most likely to
need a full export to reconcile a billing question) got silently
truncated. Replaced the cap with a 50,000-row circuit breaker that
should never fire in normal use (logged as an error, not a warning,
if it ever does) and bumped the page size from 500 to 1,000 to cut
round trips.

Removing the cap surfaced a real, pre-existing bug in
getUserUsageLogs's cursor pagination: a raw `sql` template embedded a
JS Date object directly as a bound parameter, which the postgres
driver can't serialize (unlike drizzle's typed gte/lte operators,
which already handle Date correctly elsewhere in the same function).
It only ever manifested past the first page, which nothing before
this export route's tight multi-page loop reliably exercised.
Replaced the raw sql template with drizzle's typed lt/eq/or/and
operators, matching the pattern already proven correct in this file.

Verified live: seeded 6,000 rows (past the old cap) and confirmed the
export downloads all of them in one request with credits reconciling
exactly against the total.
…dy has it

The export loop holds the previous page's rows in memory, so its next
cursor's createdAt is already known — getUserUsageLogs was still
re-resolving it via an extra DB round trip every page regardless.
Added an optional cursorCreatedAt to skip that lookup when provided;
the list route's existing callers are unaffected since they don't
pass it. Verified live: zero cursor-lookup queries fired across a
3,500-row / 4-page export that previously issued one per page.
…page/call

Cursor Bugbot caught this: the list route apportioned each page's
rows against only that page's own dollar total, while the export
apportioned every exported row against the complete set's total.
Since apportionment depends on the full set, the same log could show
a different creditCost between the list and the export, or even
between two pages of the same "Load more" list — and the sum of every
loaded row could visibly drift from the "Total" header shown above
them once more than one page had loaded.

Extracted getUsageCreditsByLogId — a single, shared, whole-filter
apportionment lookup both routes now call instead of each computing
their own subset locally. The list route calls it once per page
request (same cost profile as the summary aggregate it already pays
for every page); the export calls it once before its pagination loop,
not per page, keeping the round-trip count this session's earlier fix
already reduced. Also extracted the condition-building shared by the
main query, the summary aggregate, and this new lookup into one
buildUsageLogConditions helper, removing a third copy of that logic.

Verified live: summed every row across 4 "Load more" pages and
confirmed it now matches the reported total exactly (previously could
drift), and confirmed the list and the export produce byte-identical
credit sequences for the same rows.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/users/me/usage-logs/export/route.ts
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

…faulted

startDate/endDate had no sensible static default (they're only ever
meaningful mid-custom-range), so defaulting them to '' via
.withDefault('') meant switching back to a preset left the URL
carrying startDate=&endDate= instead of dropping the params entirely.
Made them nullable (no .withDefault) instead, matching the identical
fields in the main Logs page's own search-params.ts. Verified live —
switching from a custom range back to a preset now clears both params
from the URL completely.
Adds an Export chip to the top-right of the Audit Logs page (via
SettingsPanel's actions slot — the same header mechanism the Credit
usage page uses), downloading every audit log matching the current
search/type/date filters as CSV.

- New GET /api/audit-logs/export route: same session + enterprise
  admin/owner gating as the existing list route, reuses the shared
  buildFilterConditions/buildOrgScopeCondition/queryAuditLogs helpers
  (already using drizzle's typed operators for cursor pagination, not
  the raw-sql-with-embedded-Date pattern fixed elsewhere this
  session), and the same fetch+blob+X-Export-Truncated pattern the
  Credit usage export already established.
- Capped at 10,000 rows (not the 50,000 used for a personal credit
  ledger) — an org's audit trail can genuinely grow much larger than
  one user's usage history, so this is sized for "a reasonable audit
  review window," with truncation surfaced via a toast rather than
  silently dropped.
- Bumped the API-validation-contract audit's route-count baseline for
  the new route.

Verified live against a real enterprise org: switched to "All time,"
exported ~750 real audit log rows, confirmed formatting (quoted
descriptions, actor email fallback) and correct filter scoping.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/app/api/users/me/usage-logs/route.ts
…block export during stale data

Cursor Bugbot caught two real issues:

1. The compact Billing summary glance (limit=1) only ever reads
   summary.totalCredits, but the list route unconditionally ran
   getUsageCreditsByLogId's whole-filter scan on every call including
   this one — pure wasted work for a caller that discards the result.
   Added an includeCredits query flag (default true, using the shared
   booleanQueryFlagSchema) so useUsageSummary can opt out; the main
   paginated view keeps it on since it genuinely needs per-row values.

2. Export stayed enabled while useUsageLogs held stale rows via
   keepPreviousData mid-filter-transition — a user could change the
   period/range and click Export before the new data loaded, exporting
   against the new filter while the table still showed the old one.
   Export is now also disabled while isPlaceholderData is true.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

Comment thread apps/sim/ee/audit-logs/components/audit-logs.tsx Outdated
Comment thread apps/sim/lib/billing/core/usage-log.ts
…uring stale data

Cursor Bugbot caught two more real issues on the latest push:

1. Same stale-export bug as the earlier Credit usage fix, this time in
   Audit Logs: Export stayed enabled while useAuditLogs held prior
   rows via keepPreviousData, so it could export against a
   just-changed filter while the table still showed the old one. Now
   also disabled while isPlaceholderData is true.

2. getUsageCreditsByLogId had no ORDER BY before apportionCredits's
   largest-remainder tie-break, so which row absorbed a tied
   remainder credit depended on undefined Postgres row order — the
   same event's displayed credit could flip between calls (list vs.
   export, or even two successive requests). Added the same
   `orderBy(desc(createdAt), desc(id))` the main list query already
   uses, making the tie-break reproducible.

Verified live: 3 identically-costed rows produced the same tie-break
winner across 3 repeated requests (previously order-dependent).
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

The compact Billing glance only branched on isPending, so once
useUsageSummary settled into an error state, totalCredits stayed
undefined and formatCreditsLabel(0) rendered "0 credits" — visually
identical to genuinely having no usage this period. Now shows the
same neutral "—" placeholder for isError as it already does for
isPending.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit ecf5505. Configure here.

…ccounts

Greptile (P1) caught this: hiding the "View usage logs" link on the
Billing page for enterprise accounts doesn't stop direct navigation —
anyone with the URL (bookmark, shared link, browser history) could
still reach the full page and its CSV export, which enterprise
accounts were never supposed to see at all (billing is managed
out-of-band for them).

Added a server-side check in page.tsx before anything renders:
resolve the session, look up the highest-priority subscription, and
redirect to /settings/billing if it's enterprise — matching how
getHighestPrioritySubscription is already used elsewhere for
server-side plan checks, rather than relying on a client-side-only
conditional the way the Billing page's inline section does.

Also fixes loading.tsx: it was a Server Component (no directive)
passing a raw icon function reference into the client Chip component,
which fails RSC serialization. Added 'use client'.

Verified live in a real browser against both an enterprise account
(redirects to Billing before any credit-usage content renders) and a
non-enterprise account (reaches the page normally).
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 17a5a73. Configure here.

@waleedlatif1 waleedlatif1 merged commit 759dddb into staging Jul 4, 2026
18 checks passed
@waleedlatif1 waleedlatif1 deleted the feat/credit-usage-page branch July 4, 2026 02:37
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.

1 participant