v0.7.22: competitor pages, seo fixes, billing usage table#5393
Conversation
…mbeds, 2K players, full sequencing) (#5382) * academy: Chat section (intro, building) + Agents tool-calling and skills pages, sequenced in meta.json — pages for the four approved videos, blob src pattern, chapters offset by each recorded intro * academy pages: Related documentation links point at real docs routes (mothership/agents/logs-debugging/deployment), not other academy videos * academy: Tables — Workflow Columns page (combined-cut chapters, receipts pedagogy), sequenced after tables/intro * academy set 2: five new pages (agents/block, agents/memory, knowledge-bases/connectors, tables/operations, files/object) + retimed chapters on the redone tables/files intros (recomposed/working-day cuts) + full meta.json sequencing — Chat · Agents(5) · Tables(3) · Files(2) · KB(2) * academy pages: every page shows the VIDEO's workflow (new academy-video-workflows.ts registry — invoice intake, Qualify, memory, table ops, tools/skills agents, support-desk, content-agent) + plain pedagogical headings throughout (no poetry: 'The warm and cold split' → 'The same agent, with and without memory', 'From one pass to a loop' → 'What tools change', etc.); files/intro's embed swapped to the video's machine * academy: all video players point at the 2K blob set (academy/<slug>.mp4) — 20 pages rewritten from academy-preview/*-light-with-intro, files/intro plays the new files-intro cut, workflows/logs gains its src (video now exists); use-case placeholders stay src-less (no videos yet) * biome: format academy-video-workflows.ts * fix (cursor/greptile): support-desk condition block uses branches + rows:[] (the renderer's real fields — conditions: was never read, so Urgent? rendered without if/else handles and branch edges dangled); bgColor aligned to the product condition example * biome: format meta.json (CI lint:check) * fix (cursor): Start exposes <start.input>, not <start.ticket>/<start.idea> — support-desk Triage and content-agent Writer message rows now match the product's Start output (same pattern as the file's other workflows) * academy lesson material (CTO ask): FAQ on every lesson page + the index (grounded in the audited codebase facts: memory modes, the ten table ops, coerce/refuse, deploy surfaces + draft-vs-deployed, file parser formats; the FAQ component emits FAQPage JSON-LD for SEO); em-dashes 202 → 0 with hand-fixed splices; added copy where sparse (agent block placement, when-to-use-memory); SEO phrasing sprinkled naturally into FAQ answers ('visual platform for building AI workflows and agents', 'no-code', model names), not over-indexed * fix: quote frontmatter descriptions that gained colons in the em-dash pass (unquoted YAML scalars with a second colon broke every page)
* feat(comparison): add Sim vs Competitor comparison pages - New /comparison hub + /comparison/[provider] detail pages for Sim vs 16 competitors (n8n, Zapier, Make, Gumloop, Workato, Retool, Pipedream, OpenAI AgentKit, Tines, StackAI, Power Automate, Vellum, Claude Cowork, Langflow, Flowise), each with ~60 sourced, dated facts across platform, AI capabilities, integrations, pricing, security, observability, and support - Data layer at lib/compare/data (types, per-competitor profiles) is UI-free and independently auditable - BreadcrumbList, ItemList, and FAQPage JSON-LD per page; sitemap picks up all pages automatically via ALL_COMPETITORS - Two new fact categories (parallel execution, Agent2Agent protocol) researched and sourced against every competitor's own docs * improvement(comparison): neutralize tone across competitor and Sim fact copy - Remove promotional/superlative adjectives applied to competitors (Zapier "one of the largest catalogs", Workato "notably wide", Retool "seamless"/"trusted solution", OpenAI "cutting-edge", Flowise "deep"/"mature"/"most widely adopted", Make "robust") - Reword the few places Sim's own accurate limitations read as unflattering rather than neutral fact (dynamic tool use, model fallback, durability, async execution, support channels, tracing) without changing the underlying facts - Restore the "Yes:"/"No:" value prefix on two Sim facts where it was accidentally dropped during rewording, since FactValue depends on that prefix to render the check/X status icon * fix(comparison): address review findings from Greptile and Cursor Bugbot - Fix parseFactValue regex to require a word boundary after Yes/No, so values like "Not documented"/"Not publicly documented" no longer get misread as a boolean "No" and render as neutral text again (Cursor) - Reword the self-hosting FAQ question to drop the false presupposition that the competitor doesn't self-host, which was wrong for n8n, Langflow, and Flowise (Greptile) - Rename isLastRow -> isNotLastRow in comparison-table.tsx to match what the variable actually computes (Greptile) - Move critters to devDependencies; it's a next build -time-only CSS inliner, not needed at runtime (Greptile) - Sitemap lastModified for comparison pages now matches the max(Sim, competitor) verified-date logic each page's own JSON-LD dateModified already uses, so the two never disagree (Cursor) * fix(comparison): fix doubled Yes prefix and missing period in hub FAQ - Two hub FAQ answers prepended "Yes." to fact values that already start with "Yes:", producing "Yes. Yes: ..." in visible copy and FAQPage JSON-LD. Export and reuse ensurePeriod instead of a hardcoded prefix. - The integrations-count FAQ answer ran two sentences together with no period before "Combined with...". Fixed with the same ensurePeriod helper. * improvement(comparison): remove redundant Key differences at a glance section The 5 facts it previewed (self-hosting, environment promotion, human-in-the-loop, pricing model, data residency) are the exact same rows shown again immediately below in the full comparison table, so the section read as pure repetition for a human scrolling past it. * fix(comparison): strip Yes/No prefix before lowercasing in summarizeFact summarizeFact fed the raw fact value through lowercaseFirst even when it started with "Yes: "/"No: ", producing broken mid-sentence FAQ text like "n8n: yes: many providers via dedicated Chat Model nodes." Strip the boolean prefix first, matching Greptile's suggested fix.
…me-validation errors (#5351) * improvement(tables): harden pagination for short pages and surface name-validation errors * fix(tables): align getNextPageParam tests with count-based termination * improvement(tables): lift the 10K-char per-string-cell limit
…I, and LangChain (#5384) * feat(comparison): add Microsoft Copilot Studio, OpenClaw, Dust, CrewAI, and LangChain - 5 new "Sim vs Competitor" profiles (now 20 total), each with ~58 independently sourced facts, standout features, and limitations, researched against each vendor's own docs/pricing/GitHub - New brand icons: MicrosoftCopilotIcon, OpenClawIcon, DustIcon, LangChainIcon, CrewAIIcon - isWorkflowBuilder: false for OpenClaw, CrewAI, and LangChain since they're a personal agent runtime and code-first frameworks rather than visual workflow builders, so their FAQ asks a category-clarifying question instead of a peer feature-gap one - Independent tone audit (no over-praising competitors, no unflattering Sim framing) and a fresh accuracy re-verification pass (50 highest- stakes facts across all 5 profiles, all confirmed against live sources) both came back clean * fix(comparison): fix two more FAQ text-mangling bugs found during final audit - lowercaseFirst only guarded against 2+ CONSECUTIVE leading capitals (acronyms like "AI"/"SSO"), so CamelCase brand names with a single leading capital (LangChain, OpenClaw, CrewAI) got their first letter wrongly lowercased ("langChain provides..."). Now checks for 2+ uppercase letters anywhere in the leading word, which covers both acronyms and CamelCase brand names. - parseFactValue and summarizeFact's boolean-prefix stripping only recognized "Yes:"/"No:" (colon), but "Yes, ..."/"No, ..." (comma) is an equally common phrasing already used across ~15 existing facts (stackai, pipedream, workato, zapier, etc.), so those facts kept their leading comma when stitched into an FAQ answer (e.g. "StackAI: , broad support..."). Both now accept either separator. Found by systematically sweeping every "Sim vs X" FAQ answer across all 20 competitor pages for garbled/mis-cased text, not just the 5 newly added ones. * feat(comparison): add sub-workflow composition and loop-block facts - Two new universal comparison facts across all 20 profiles: subWorkflows (calling a saved workflow as a reusable step inside another) and loopIteration (a dedicated sequential for-each/while loop container, distinct from concurrent Parallel execution) - Both are real Sim capabilities (Workflow block, Loop block) verified directly against the codebase and docs.sim.ai - Findings are genuinely mixed, not uniformly favorable: n8n, Zapier, Make, Workato, Retool, Power Automate, Gumloop, Vellum, Stack AI, Tines, Langflow, Flowise, Microsoft Copilot Studio, and LangChain all have some form of sub-workflow calling; Zapier and Gumloop's loop primitives run concurrently rather than sequentially (marked "Partial", not "No"); Pipedream, OpenAI AgentKit, Claude Cowork, CrewAI, Dust, and OpenClaw genuinely lack one or both * feat(comparison): add third-party integration vetting fact New universal comparison fact across all 20 profiles: thirdPartyVetting, whether a platform's integrations/tools/skills come from a vetted first-party catalog vs. an open marketplace where any third party can publish executable code with lighter or no vendor security review. Directly relevant given OpenClaw's ClawHub marketplace has documented incidents (283 skills, ~7.1% of the registry, found leaking credentials; 24 accounts distributing 600+ malicious skills before scanning existed). Findings are honest and mixed, not uniformly favorable: Gumloop, Retool, and Tines are first-party-only like Sim (marked "Yes"); n8n, Zapier, Make, Workato, and OpenAI AgentKit have partial vetting on an open or semi-open ecosystem; Pipedream and OpenClaw are open marketplaces with documented security incidents. * fix(comparison): correct n8n supply-chain attack download count Independently re-verified the cited Hacker News article: the primary malicious package had 4,241 downloads listed (not "3,400 weekly" as previously written, a number not actually supported by the source). * fix(comparison): fix duplicated competitor name in first FAQ answer Every competitor's oneLiner is already a complete "{Name} is ..." sentence, so prepending "${name} is " before it was always redundant. Before this session's lowercaseFirst fix, the duplication rendered in mixed case ("Zapier is zapier is a cloud-based...") and was easy to miss; the CamelCase-name fix made it fully literal and obvious ("CrewAI is CrewAI is..."), which is what Cursor Bugbot caught. Fixed by using the oneLiner directly (via ensurePeriod) instead of re-prepending the name. Verified fixed across all 20 pages via live curl, not just the CrewAI case Cursor flagged. * improvement(comparison): cite Sim's own docs alongside code for 4 facts Added docs.sim.ai citations (Roles and Permissions, BYOK, Debugging retrieval, Function block) as primary sources for rbac, byok, kbChunkVisibility, and customCodeSteps, which previously cited only GitHub source code with no user-facing documentation reference. Verified all 4 doc URLs resolve (200).
…#5387) * fix(billing): stop showing "View all" when there are no more invoices Show fewer invoices (10) and derive hasMore from the finalized-invoice count instead of Stripe's raw has_more, which counted drafts we filter out client-side and could report more invoices than actually exist. * chore(billing): move invoice pagination explanation into TSDoc Extract collectFinalizedInvoices with a TSDoc comment explaining the Stripe has_more/draft-filtering rationale, instead of an inline comment block. * fix(billing): don't hide View all when the page-cap is hit inconclusively Greptile P1: if MAX_STRIPE_PAGES is exhausted while the finalized count sits exactly at MAX_INVOICES and Stripe still has_more, hasMore was silently returned as false. collectFinalizedInvoices now also returns whether Stripe's cursor was still open at exit, and the route ORs that into hasMore so the safety cap can never suppress "View all". Also asserts starting_after cursor propagation across pages and adds a test for the safety-cap-hit case.
…#5386) * fix(sso): support skipping the OIDC UserInfo endpoint at registration * fix(sso): cap OIDC discovery fetch at 10s to avoid stalling registration * test(sso): default-mock discovery fetch so intent is explicit * fix(sso): prefer client_secret_post and surface discovery failure reasons * fix(sso): always resolve token auth method and skip SSRF-checking a discarded userInfoEndpoint
…es (#5388) * fix(seo): fix GSC indexing issues, remove unused academy/partners pages - robots.ts: unblock /chat/ (page-level noindex now gates gated/inactive deployments instead), drop the now-vestigial blog-tag/link-preview carve-out - next.config.ts: add missing redirects for renamed integration slugs (sap-s-4hana, calcom), removed /partners, and removed /academy - fix missing canonical/noindex on filtered catalog pages (integrations, models, blog, careers, pricing) causing GSC "duplicate, Google chose different canonical" - standardize page titles to "Page | Sim, the AI Workspace" across the board - remove the academy marketing pages and partner program page (content consolidated into docs.sim.ai/academy); drop the unused academy_certificate table via migration and strip the sandbox-mode plumbing from the workflow editor that only academy ever used * fix(migrations): defer academy_certificate table drop to a follow-up PR CI's expand/contract migration safety check correctly flagged this: the academy_certificate DROP TABLE was bundled in the same PR as removing the code that reads/writes it (the certificates API route had no feature-flag guard of its own, so it was reachable independent of the marketing pages being disabled). Dropping the table in the same deploy risks breaking any pod still running the old code during a rolling deploy. Restores the table/enum in schema.ts and the test mock, and removes the 0254 migration. The table drop should ship in its own PR once this one's code removal is confirmed live. * fix(seo): drop dead revalidate exports on searchParams-driven pages Any Server Component in the route tree reading searchParams forces the whole route to fully dynamic per-request rendering, which overrides ISR — revalidate is a silent no-op once that happens. True on pricing/careers because generateMetadata now parses searchParams directly, and was already true on blog before this PR (its page body already read searchParams). Flagged by Greptile on pricing; same root cause applies to all three.
#5389) * improvement(styling): use a consistent branded text-selection color app-wide Previously no ::selection rule was defined anywhere, so text selection fell back to the browser/OS default, which dims when a window loses focus and made pages look inconsistent side by side. Adds one global rule using the existing --selection token. * fix(styling): resolve markdown-selection conflict, use --white token Remove the local link/strikethrough ::selection color overrides in rich-markdown-editor.css now that the global ::selection rule already forces uniform white text on every selection. Also swap the literal #ffffff for the existing --white design token.
…ial hedging (#5390) * improvement(compare): tighten comparison-page copy, cut self-referential hedging Across sim.ts and all 20 competitor profiles, remove phrasing that describes a claim's own provenance/reliability (e.g. 'marketing claim', 'marketing copy states', 'as of this check') in favor of stating the fact directly. Also trims overlong run-on sentences into shorter, more direct ones. No numbers, dates, comparisons, confidence levels, or sources changed. * fix(compare): avoid accidental boolean-icon misparse from copy tightening Three facts whose tightened wording started with a bare 'No ' now matched the Yes/No boolean-icon convention (parseFactValue), collapsing a nuanced 'Partial' or plain descriptive sentence into a bare X icon. Rephrase with 'Not'/'not' so these render as full text again, matching pre-edit behavior.
The `{ ...base, ...(isFiltered && { robots: { index: false, follow: true } }) }`
faceted-navigation noindex pattern was duplicated verbatim across five
catalog pages (integrations, models, blog, careers, pricing). Extracted to
lib/landing/seo.ts alongside buildLandingMetadata, giving the pattern a
name and a single point of change. Pure refactor — verified byte-identical
rendered output (robots meta + canonical) on baseline and filtered variants
of all five pages before and after.
* feat(billing): expose credit usage log in Billing settings
Add a "Credit usage" section under Billing, below Invoices, showing a
paginated, period-filterable list of individual credit-consuming
events (model, tool, and fixed charges) for every plan except
Enterprise. Wires the existing (previously unused) usage-logs backend
to a proper contract, React Query hook, and emcn-styled UI:
- getUsageLogsContract in contracts/user.ts, broadened the source enum
to match the real usage_log schema
- Rewrote the route to use parseRequest per the API boundary rules
- useUsageLogs infinite-query hook, keyset-paginated
- CreditUsageSection: period dropdown, total-credits summary, row
list with source badges, "Load more"
- Extracted formatCreditsLabel in conversion.ts as the shared
formatter for already-converted integer credits
* fix(billing): move usage-log key factory out of the 'use client' boundary
Greptile P2: usageLogKeys lived in the 'use client' usage-logs.ts hook
file — importing it from a server component (e.g. a future prefetch)
would resolve to a client-reference stub and crash at build/SSR, the
same class of bug that hit tables' key factory before. Extracted to
hooks/queries/utils/usage-log-keys.ts, matching table-keys.ts and
folder-keys.ts.
Also renamed a shadowed `source` map param in the route to `sourceKey`
for clarity.
* chore(billing): dedupe the usage-log period literal type
The '1d' | '7d' | '30d' | 'all' union was hand-typed in three places
across usage-logs.ts and credit-usage-section.tsx. Extracted
usageLogPeriodSchema in the contract and derived UsageLogPeriod from
it, so the hook, the component, and the key factory all share one
definition instead of risking drift.
* improvement(billing): move credit usage period filter into the URL
/cleanup pass (nuqs rule, react-query-best-practices, emcn review):
- period was a plain useState, but it's exactly the kind of shareable
list filter sim-url-state.md calls out for nuqs — migrated to
billingParsers/useQueryStates so the selection deep-links, survives
reload, and matches every sibling settings section (recently-deleted,
inbox, teammates). Derives its literal values from
usageLogPeriodSchema instead of a fourth copy of the same union.
- Removed an unjustified showSelectedCheck={false} on the period
ChipDropdown — the one other usage in the codebase is a one-shot
action menu with no persistent selection; this is a real filter and
should show the check like the default intends.
- Added placeholderData: keepPreviousData to useUsageLogs — period is
a variable query key, so without it switching periods flashed the
loading empty-state instead of smoothly transitioning.
Verified live: deep-link with ?period=7d pre-selects and loads
correctly, switching periods updates the URL, and the selection
survives a hard reload.
|
Too many files changed for review. ( Bypass the limit by tagging |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryLow Risk Overview Marketing adds a SEO applies shared Reviewed by Cursor Bugbot for commit d508201. Bugbot is set up for automated code reviews on this repo. Configure here. |
generateMetadata queried the DB with no error handling, unlike the identical query in api/chat/[identifier]/route.ts (which already wraps it in try/catch). There's no error.tsx under app/(interfaces)/chat/ — metadata resolution errors aren't caught by route-segment error boundaries at all, only the root global-error.tsx — so a DB hiccup during this lookup would take the whole page down to a generic error page instead of just failing to determine indexability. Catches the error, logs it, and defaults to noindex: if we can't confirm the deployment is safely public, that's the correct SEO default anyway, not a reason to crash the request. Flagged by Cursor Bugbot on #5388 (already merged); this ships the fix as a standalone follow-up since the underlying code is already live.
* fix(gmail): strip CR/LF from header values before MIME assembly Adds sanitizeHeaderValue and applies it to to/cc/bcc/subject/ inReplyTo/references and the attachment filename in buildSimpleEmailMessage and buildMimeMessage before they're placed into MIME header lines. * fix(gmail): sanitize attachment mimeType in Content-Type header attachment.mimeType was written verbatim into the Content-Type header, unlike the other header fields this PR sanitizes. Route it through the same sanitizeHeaderValue helper for consistency.
…ing (#5397) * fix(tables): verify workflow belongs to table's workspace before binding Add a check that a table workflow group's workflowId resolves to an active workflow in the table's own workspace, in both the create and update handlers, before it is persisted. * fix(tables): reorder JSDoc for mapWorkflowGroupError Greptile flagged the JSDoc for mapWorkflowGroupError as orphaned after validateWorkflowInWorkspace was inserted between the comment and the function it documented. Move the comment back above its function.
…on validation (#5400) * fix(guardrails): authorize vertexCredential before use in hallucination validation The guardrails validate route now checks credential access via authorizeCredentialUse before passing a caller-supplied vertexCredential into hallucination validation, matching the existing pattern already used in the providers route for the same field. * fix(guardrails): gate vertexCredential authorization on the resolved provider Only run the credential check when the model actually resolves to vertex, matching the providers route's gating exactly, and drop a redundant fallback now that workflowId is already guaranteed present at that point.
…be/revoke (#5398) Discovery and registration during the MCP OAuth start flow were using the default global fetch. probe.ts and revoke.ts already route these calls through createSsrfGuardedMcpFetch(); this brings the start route in line with the same pattern.
…5399) Mirrors the same wiring already used by probe.ts and revoke.ts, so the callback's token-exchange request goes through the same guarded fetch as the rest of the OAuth flow.
…ssion (#5404) Fallback multipart upload route (/api/files/upload) had no workspace permission check for execution-context uploads, unlike the primary presigned-upload route which requires write/admin. Mirror that gate so both paths enforce the same access control.
…s and modals (#5394) * fix(ui): surface silent validation and rejection errors across editors and modals * improvement(knowledge): toast chunk validation errors instead of inline footer text * fix(tables): reject invalid date/number in expanded cell editor, dedupe invalid-input toasts * fix(knowledge): toast every failed chunk validation attempt, replacing the previous toast * fix(knowledge): dismiss stale validation toast once content validates * revert(ui): drop chat identifier error rendering and profile-name toast
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d508201. Configure here.
… CSV export (#5405) * fix(billing): apportion per-row credit costs so they sum to the page 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. * fix(billing): dim stale credit usage rows while a new period loads 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. * fix(billing): show "<1 credit" for rows apportioned to 0 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. * fix(audit-logs): fix broken Custom range picker, trim time-range presets 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. * feat(billing): dedicated Credit usage page with date-range filter and 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. * refactor(billing): move workflow-name enrichment into getUserUsageLogs, 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. * fix(billing): drop Dollar cost from the CSV export, strip inline comments 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). * fix(billing): export honors partial custom date range, surfaces truncation 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. * fix(billing): remove the export's arbitrary row cap, fix a cursor pagination 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. * perf(billing): skip the redundant cursor lookup when the caller already 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. * fix(billing): apportion credits over the whole filtered set, not per 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. * fix(billing): make custom-range startDate/endDate nullable, not '' defaulted 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. * feat(audit-logs): add CSV export, matching the Credit usage page pattern 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. * fix(billing): skip wasted credit apportionment on the summary fetch, 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. * fix(billing): deterministic apportionment order, block audit export during 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). * fix(billing): distinguish a failed summary fetch from zero usage 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. * fix(billing): gate the credit-usage page server-side for enterprise accounts 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).

fix(ui): surface silent validation and rejection errors across editors and modals #5394