Skip to content

Improve dashboard navigation performance#350

Merged
taqh merged 10 commits into
mainfrom
codex/dashboard-navigation-performance
May 30, 2026
Merged

Improve dashboard navigation performance#350
taqh merged 10 commits into
mainfrom
codex/dashboard-navigation-performance

Conversation

@mezotv
Copy link
Copy Markdown
Member

@mezotv mezotv commented May 28, 2026

Description

Improves CMS dashboard navigation performance by removing server/client data waterfalls and reducing repeat-navigation loading states.

  • Fetches initial dashboard data on the server for home metrics, posts, media, authors, categories, tags, API keys, custom fields, and webhooks.
  • Reuses shared dashboard query helpers from both Server Components and API route handlers.
  • Adds sidebar route/query prefetching for dashboard destinations.
  • Avoids repeated active-workspace session updates during workspace layout rendering.
  • Uses selected layout segments for sidebar active state instead of full pathname reads.
  • Replaces the dashboard route spinner with a table-shaped skeleton and adds a short dynamic router cache window for repeat navigation.

Motivation and Context

Dashboard navigation was slow because page transitions mounted client pages that then fetched data through internal API routes. Repeated back-and-forth navigation could also show the route-level skeleton even when React Query already had data, because the dynamic RSC payload was refetched before client cache mounted.

This change follows the Next.js best-practice guidance to fetch internal reads in Server Components, avoid waterfalls, prefetch likely navigation targets, and keep route loading states stable.

How to Test

  1. Start the CMS app.
  2. Open a workspace dashboard.
  3. Navigate between Home, Posts, Media, Authors, Categories, Tags, API Keys, Custom Fields, and Webhooks.
  4. Verify initial page content renders without waiting for a client-side API fetch after the route loads.
  5. Navigate back and forth between recently visited dashboard pages and verify the skeleton does not flash during the router cache window.
  6. Create/update/delete dashboard resources and verify the existing React Query invalidation flows still refresh the tables.

Checks run:

pnpm --filter cms exec biome check src/lib/dashboard-prefetch.ts src/lib/queries/dashboard.ts src/lib/queries/workspace.ts src/lib/search-params.ts src/lib/constants.ts src/components/nav/app-sidebar.tsx src/components/nav/nav-main.tsx src/components/nav/nav-settings.tsx 'src/app/(main)/[workspace]/layout.tsx' 'src/app/(main)/[workspace]/(dashboard)' src/app/api/posts/route.ts src/app/api/media/route.ts src/app/api/categories/route.ts src/app/api/tags/route.ts src/app/api/authors/route.ts src/app/api/keys/route.ts src/app/api/webhooks/route.ts src/app/api/fields/route.ts src/app/api/metrics/usage/route.ts next.config.ts

Targeted TypeScript checks for the changed files showed no new errors. Full tsc --noEmit is still blocked by the existing Zod/react-hook-form resolver version mismatch in unrelated files.

Screenshots (if applicable)

N/A

Video Demo (if applicable)

N/A

Types of Changes

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • ⚠️ Breaking change (fix or feature that alters existing functionality)
  • 🎨 UI/UX Improvements
  • ⚡ Performance Enhancement
  • 📖 Documentation (updates to README, docs, or comments)

Summary by CodeRabbit

Release Notes

  • New Features

    • Added loading skeletons to dashboard pages for improved visual feedback during navigation.
  • Performance

    • Optimized dashboard data loading by implementing server-side prefetching, enabling content to display instantly on page load with pre-populated initial data.
  • Bug Fixes

    • Improved navigation state detection in sidebar using layout segments instead of pathname matching.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 28, 2026

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

Project Deployment Actions Updated (UTC)
marble-app Ready Ready Preview, Comment May 30, 2026 1:14pm
marble-web Ready Ready Preview, Comment May 30, 2026 1:14pm

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR migrates the CMS dashboard from client-side to server-side data fetching by introducing workspace-scoped query abstractions, converting dashboard pages to async server components that prefetch data, hydrating client components via props, delegating API routes to new queries, and updating navigation to use layout segments for active state detection.

Changes

Dashboard Server-Side Rendering Migration

Layer / File(s) Summary
Dashboard query abstractions and workspace helpers
apps/cms/src/lib/queries/dashboard/*, apps/cms/src/lib/queries/workspace.ts, apps/cms/src/lib/search-params.ts
New server-only query functions centralize dashboard data fetching: getDashboardAuthors, getDashboardCategories, getDashboardTags, getDashboardPosts, getDashboardMedia, getDashboardApiKeys, getDashboardWebhooks, getDashboardCustomFields, getDashboardUsageMetrics. Adds getWorkspaceLayoutData for consolidated workspace resolution and loadMediaPageFilters for media page filters.
Centralized dashboard type definitions
apps/cms/src/types/dashboard.ts, apps/cms/src/components/{categories,keys,posts,tags}/columns.tsx
Consolidates CMS domain types (Category, Tag, Post, APIKey) into shared @/types/dashboard module; column components import and re-export types instead of defining them locally.
API route delegation and simplification
apps/cms/src/app/api/{authors,categories,fields,keys,media,metrics/usage,posts,tags,webhooks}/route.ts
All API routes now delegate to dashboard query functions instead of inline Prisma queries, reducing duplicated logic and enabling data shaping centralization.
Server-side page components with data fetching
apps/cms/src/app/(main)/[workspace]/(dashboard)/{(home),authors,categories,media,posts,settings/{fields,keys,webhooks},tags}/page.tsx
Dashboard pages become async server components that await route params, resolve workspace ID via getDashboardWorkspaceId, call notFound() when invalid, fetch data via dashboard queries, and pass initial data to client components.
Client component initialization via props
apps/cms/src/app/(main)/[workspace]/(dashboard)/{(home),authors,categories,media,posts,settings/{fields,keys,webhooks},tags}/page-client.tsx
All page-client components updated to accept optional initial* props and wire them into React Query initialData for immediate server-provided rendering.
Layout segment-based navigation routing
apps/cms/src/components/nav/{app-sidebar,nav-main,nav-settings}.tsx
Navigation components switch from usePathname() to useSelectedLayoutSegments() for active state detection, improving reliability and enabling SSR-compatible route tracking.
Workspace layout data consolidation
apps/cms/src/app/(main)/[workspace]/layout.tsx, apps/cms/src/lib/queries/workspace.ts
Workspace layout now uses single getWorkspaceLayoutData helper combining access validation and data fetching; conditionally updates active workspace based on organization ID matching.
Dashboard loading skeleton components
apps/cms/src/app/(main)/[workspace]/(dashboard)/{(home),loading,media,posts}/loading.tsx
New loading page components render skeleton placeholders for home, media, and posts dashboards, improving perceived performance during server data fetching.
Activity indicator component and dashboard animations
apps/cms/src/components/ui/{activity-indicator.tsx,.module.css}, apps/cms/src/components/{layout/wrapper.tsx,shared/page-loader.tsx}
Adds new ActivityIndicator component with CSS fade animations; updates DashboardBody and PageLoader to use new indicator with reduced-motion support.
Configuration and infrastructure updates
apps/cms/next.config.ts, apps/cms/src/lib/constants.ts
Adds Next.js dynamic stale times (60s), extends WORKSPACE_SCOPED_PREFIXES with custom-fields, and adds cdn.marblecms.com to allowed remote image patterns.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • usemarble/marble#43: Modifies the same workspace data initialization flow in getInitialWorkspaceData that this PR refactors to delegate to getWorkspaceLayoutData.
  • usemarble/marble#194: Changes the media query and pagination flow at apps/cms/src/app/api/media/route.ts and media dashboard behavior that this PR centralizes into getDashboardMedia.
  • usemarble/marble#249: Implements underlying usage tracking and usage_event schema that feeds the new getDashboardUsageMetrics query introduced in this PR.

Suggested reviewers

  • prateekbisht23

🐰 Hops through dashboard routes with glee,
Server-side data now flows so free!
No more client spinners, cache seeds the day,
Layout segments guide the nav's way!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Improve dashboard navigation performance' clearly and concisely summarizes the main change, which is a performance enhancement to dashboard navigation by eliminating data waterfalls and reducing loading states.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all required template sections: Description (clear overview of changes), Motivation and Context (explains the problem and solution approach), How to Test (step-by-step verification instructions), Types of Changes (multiple boxes checked), and technical details about checks run.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/dashboard-navigation-performance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page-client.tsx (1)

18-38: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rethrow fetch failures so query state can fail correctly.

The catch path toasts but returns implicitly, so the query resolves with undefined instead of entering error state. That can hide failures and replace hydrated data with empty state.

Proposed fix
     queryFn: async () => {
       try {
         const response = await fetch("/api/authors");
         if (!response.ok) {
           throw new Error("Failed to fetch authors");
         }
         const data: Author[] = await response.json();
         return data;
       } catch (error) {
         toast.error(
           error instanceof Error ? error.message : "Failed to fetch authors"
         );
+        throw error instanceof Error
+          ? error
+          : new Error("Failed to fetch authors");
       }
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cms/src/app/`(main)/[workspace]/(dashboard)/authors/page-client.tsx
around lines 18 - 38, The queryFn for useQuery (in the authors hook that returns
{ data: authors, isLoading }) currently catches fetch errors, toasts them, and
returns implicitly causing useQuery to resolve with undefined; change the catch
block in that queryFn to rethrow the error (or return Promise.reject(error))
after calling toast.error so that useQuery enters the error state instead of
silently returning undefined—look for the queryFn inside the useQuery call that
uses QUERY_KEYS.AUTHORS(workspaceId), workspaceId, isFetchingWorkspace, and
initialAuthors and modify the catch to rethrow the caught Error.
apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page-client.tsx (1)

34-52: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t swallow category fetch errors in queryFn.

This catch block emits a toast but resolves successfully, which bypasses query error state and can render misleading empty data.

Proposed fix
     queryFn: async () => {
       try {
         const res = await fetch("/api/categories");
         if (!res.ok) {
           throw new Error(
             `Failed to fetch categories: ${res.status} ${res.statusText}`
           );
         }
         const data: Category[] = await res.json();
         return data;
       } catch (error) {
         toast.error(
           error instanceof Error ? error.message : "Failed to fetch categories"
         );
+        throw error instanceof Error
+          ? error
+          : new Error("Failed to fetch categories");
       }
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cms/src/app/`(main)/[workspace]/(dashboard)/categories/page-client.tsx
around lines 34 - 52, The queryFn for the categories useQuery swallows errors by
catching them and only calling toast.error, preventing react-query from entering
an error state; update the queryFn (the async function that calls
fetch("/api/categories")) to re-throw the caught error (or return
Promise.reject(error)) after showing toast.error (or omit the try/catch and let
react-query handle it) so that useQuery's error handling and states work
correctly instead of returning undefined successful resolution.
apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page-client.tsx (1)

34-39: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Scope initialData to only the server-matched media query key

PageClient always passes initialData: initialMedia (from media/page.tsx) while the useQuery queryKey changes with { page, perPage, search, sort, type }. With TanStack Query, when those key parts change and a new cache entry is created, initialData is applied to that new key too—so the previous page’s data can be shown while staleTime: 5m keeps it “fresh” longer than expected.

Suggested direction
 function PageClient({
   initialMedia,
+  initialMediaKey,
 }: {
   initialMedia?: MediaPaginatedListResponse;
+  initialMediaKey?: string;
 }) {
+  const currentMediaKey = JSON.stringify({ page, perPage, search, sort, type: normalizedType });
+  const shouldUseInitialData = initialMediaKey === currentMediaKey;
+
   const { data, error, isError, isLoading, isFetching } = useQuery({
     queryKey: [
       ...QUERY_KEYS.MEDIA(workspaceId!),
       { page, perPage, search, sort, type: normalizedType },
     ],
@@
-    initialData: initialMedia,
+    initialData: shouldUseInitialData ? initialMedia : undefined,

Compute/pass initialMediaKey from media/page.tsx using the same key params that the client puts into queryKey (especially type: toMediaType(filters.type) and search as the useMediaPageFilters string).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cms/src/app/`(main)/[workspace]/(dashboard)/media/page-client.tsx around
lines 34 - 39, The initialData (initialMedia) is being passed to useQuery in
PageClient without scoping it to the exact server-side query key, causing
previous-page data to be reused when client queryKey changes; compute an
initialMediaKey in media/page.tsx using the same key parts the client uses (i.e.
use the same QUERY_KEYS.MEDIA(...) shape and the same conversions:
toMediaType(filters.type) and the string from useMediaPageFilters for
search/page/perPage/sort/type), then pass initialData together with that exact
key (initialMediaKey) to the client-side useQuery in PageClient so initialData
only applies to the matching cache entry rather than every client queryKey
variant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cms/src/app/`(main)/[workspace]/(dashboard)/posts/page-client.tsx:
- Line 76: The useQuery call is seeding new queries with the original
initialPosts because initialData: initialPosts is unconditional; change it to
only supply initialPosts for the initial hydration matching the original filters
(e.g., compare current apiFilters/queryKey to the initialFilters/queryKey
captured at mount or use an isFirstHydration flag) so that when apiFilters
changes the new query does not get the stale initialPosts; update the useQuery
call (the initialData property) to return initialPosts only when the current
queryKey equals the initial queryKey (or when isFirstHydration is true),
otherwise leave initialData undefined and continue relying on placeholderData:
keepPreviousData to preserve UI during transitions.

In `@apps/cms/src/lib/dashboard-prefetch.ts`:
- Around line 69-75: The queryKey for the media prefetch uses
defaultMediaFilters (which contains search: "") but the actual fetch uses
{...defaultMediaFilters, search: null}, causing cache split; update the queryKey
(used with QUERY_KEYS.MEDIA and defaultMediaFilters) to use the exact same
normalized filters as the request (e.g., spread defaultMediaFilters and set
search: null) so the key matches the fetch used by fetchJson/getMediaApiUrl.

In `@apps/cms/src/lib/queries/dashboard.ts`:
- Around line 286-307: The dashboard currently forces requestCount to 0 in the
return mapper, so update getDashboardApiKeys to return the real usage count:
include requestCount in the Prisma/select projection (add requestCount to the
select block) and remove the hardcoded requestCount: 0 in the keys.map so you
map the actual key.requestCount value (while keeping the existing casts for type
and scopes on the keys variable).

In `@apps/cms/src/lib/queries/workspace.ts`:
- Line 213: Replace the direct console.error call used to log "Error fetching
workspace layout data:" with the project's structured logger (e.g., replace
console.error(...) with processLogger.error(...) or the module's
logger.error(...)), pass the error object as a second argument so structured
fields are preserved, and add/import the appropriate logger instance if it's not
available in this module; ensure the log message and error are recorded via the
same logging API used elsewhere in workspace-related functions (match other
usages such as processLogger or logger in this file).

---

Outside diff comments:
In `@apps/cms/src/app/`(main)/[workspace]/(dashboard)/authors/page-client.tsx:
- Around line 18-38: The queryFn for useQuery (in the authors hook that returns
{ data: authors, isLoading }) currently catches fetch errors, toasts them, and
returns implicitly causing useQuery to resolve with undefined; change the catch
block in that queryFn to rethrow the error (or return Promise.reject(error))
after calling toast.error so that useQuery enters the error state instead of
silently returning undefined—look for the queryFn inside the useQuery call that
uses QUERY_KEYS.AUTHORS(workspaceId), workspaceId, isFetchingWorkspace, and
initialAuthors and modify the catch to rethrow the caught Error.

In `@apps/cms/src/app/`(main)/[workspace]/(dashboard)/categories/page-client.tsx:
- Around line 34-52: The queryFn for the categories useQuery swallows errors by
catching them and only calling toast.error, preventing react-query from entering
an error state; update the queryFn (the async function that calls
fetch("/api/categories")) to re-throw the caught error (or return
Promise.reject(error)) after showing toast.error (or omit the try/catch and let
react-query handle it) so that useQuery's error handling and states work
correctly instead of returning undefined successful resolution.

In `@apps/cms/src/app/`(main)/[workspace]/(dashboard)/media/page-client.tsx:
- Around line 34-39: The initialData (initialMedia) is being passed to useQuery
in PageClient without scoping it to the exact server-side query key, causing
previous-page data to be reused when client queryKey changes; compute an
initialMediaKey in media/page.tsx using the same key parts the client uses (i.e.
use the same QUERY_KEYS.MEDIA(...) shape and the same conversions:
toMediaType(filters.type) and the string from useMediaPageFilters for
search/page/perPage/sort/type), then pass initialData together with that exact
key (initialMediaKey) to the client-side useQuery in PageClient so initialData
only applies to the matching cache entry rather than every client queryKey
variant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fcac1a51-af16-4f37-aa9d-2a08180f344c

📥 Commits

Reviewing files that changed from the base of the PR and between 81e0919 and 53fed0e.

📒 Files selected for processing (38)
  • apps/cms/next.config.ts
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/loading.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/fields/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/fields/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/keys/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/keys/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page.tsx
  • apps/cms/src/app/(main)/[workspace]/layout.tsx
  • apps/cms/src/app/api/authors/route.ts
  • apps/cms/src/app/api/categories/route.ts
  • apps/cms/src/app/api/fields/route.ts
  • apps/cms/src/app/api/keys/route.ts
  • apps/cms/src/app/api/media/route.ts
  • apps/cms/src/app/api/metrics/usage/route.ts
  • apps/cms/src/app/api/posts/route.ts
  • apps/cms/src/app/api/tags/route.ts
  • apps/cms/src/app/api/webhooks/route.ts
  • apps/cms/src/components/nav/app-sidebar.tsx
  • apps/cms/src/components/nav/nav-main.tsx
  • apps/cms/src/components/nav/nav-settings.tsx
  • apps/cms/src/lib/constants.ts
  • apps/cms/src/lib/dashboard-prefetch.ts
  • apps/cms/src/lib/queries/dashboard.ts
  • apps/cms/src/lib/queries/workspace.ts
  • apps/cms/src/lib/search-params.ts

Comment thread apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx Outdated
Comment thread apps/cms/src/lib/dashboard-prefetch.ts Outdated
Comment thread apps/cms/src/lib/queries/dashboard.ts Outdated
Comment thread apps/cms/src/lib/queries/workspace.ts Outdated
Copy link
Copy Markdown
Contributor

@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 (1)
apps/cms/src/lib/queries/workspace.ts (1)

213-213: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Route error logging through the structured logger.

console.error is used here; please use the project's structured logger and pass the error as a structured field.

As per coding guidelines, "Don't use console".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cms/src/lib/queries/workspace.ts` at line 213, Replace the raw
console.error call that logs the "Error fetching initial workspace data:" and
the error variable with the project's structured logger: import/use the module
logger instance (replace console.error) and call its error method, passing a
clear message and the error as a structured field (e.g., message plus { error }
or error: error.stack) so the error is recorded in the structured logs; update
the statement where console.error is used to use the logger instance and include
the error object rather than printing to console.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@apps/cms/src/lib/queries/workspace.ts`:
- Line 213: Replace the raw console.error call that logs the "Error fetching
initial workspace data:" and the error variable with the project's structured
logger: import/use the module logger instance (replace console.error) and call
its error method, passing a clear message and the error as a structured field
(e.g., message plus { error } or error: error.stack) so the error is recorded in
the structured logs; update the statement where console.error is used to use the
logger instance and include the error object rather than printing to console.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 58e1e0e0-d40d-4860-ab41-d4a5f3e1408f

📥 Commits

Reviewing files that changed from the base of the PR and between 53fed0e and a2619ee.

📒 Files selected for processing (12)
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/keys/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page-client.tsx
  • apps/cms/src/lib/dashboard-prefetch.ts
  • apps/cms/src/lib/queries/dashboard.ts
  • apps/cms/src/lib/queries/workspace.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page-client.tsx
  • apps/cms/src/lib/dashboard-prefetch.ts
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page-client.tsx
  • apps/cms/src/lib/queries/dashboard.ts

Replace the single generic table skeleton with skeletons that mirror each
route's real layout (home metrics cards, posts and media tables), and trim
the shared fallback to the simple bordered-table pages.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 29, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
marble-api 12a73f8 Commit Preview URL

Branch Preview URL
May 29 2026, 07:28 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 29, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
marble-jobs 12a73f8 Commit Preview URL

Branch Preview URL
May 29 2026, 07:28 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 29, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
marble-mcp 12a73f8 Commit Preview URL

Branch Preview URL
May 29 2026, 07:29 PM

Copy link
Copy Markdown
Contributor

@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: 3

🧹 Nitpick comments (1)
apps/cms/src/lib/queries/dashboard/usage.ts (1)

18-156: ⚡ Quick win

Optional: the two Promise.all batches run sequentially.

The second batch (Lines 75-156) doesn't depend on apiEvents/counts from the first (Lines 18-43); the in-memory bucket build is the only thing between them. Awaiting them as two separate batches adds a DB round-trip of latency. Since this PR targets navigation performance, you could merge both into a single Promise.all and build chartBuckets afterward, provided the connection pool comfortably handles the added concurrency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cms/src/lib/queries/dashboard/usage.ts` around lines 18 - 156, The two
sequential Promise.all batches cause an extra DB round-trip; merge the first
array (producing apiEvents, apiPrevPeriodCount, apiTotalCount) with the second
larger array (producing webhookTotal, webhookWeek, webhookDay,
webhookTopEndpoint, webhookEvents, mediaTotals, mediaLast30, mediaLastUpload,
recentMediaUploads) into a single Promise.all call, then build chartBuckets and
compute apiChart/apiLastPeriodCount/apiChange after that single await; locate
the arrays passed to Promise.all that populate variables named apiEvents,
apiPrevPeriodCount, apiTotalCount and the later variables webhookTotal,
webhookWeek, webhookDay, webhookTopEndpoint, webhookEvents, mediaTotals,
mediaLast30, mediaLastUpload, recentMediaUploads and combine them into one
concurrent request (ensuring your DB connection pool can handle the increased
concurrency).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cms/src/lib/queries/dashboard/posts.ts`:
- Around line 86-88: The ordering currently uses a single dynamic key ({
[field]: direction }) in getDashboardPosts which can yield nondeterministic
paging when values tie; update the orderBy in getDashboardPosts to include a
stable secondary key (e.g., include id with the same direction) so it becomes {
[field]: direction, id: direction } to match the approach used in
getDashboardMedia and ensure deterministic pagination.

In `@apps/cms/src/lib/queries/dashboard/settings.ts`:
- Around line 38-49: getDashboardWebhooks currently returns full WebhookEndpoint
records (including the sensitive WebhookEndpoint.secret) to the client; fix by
changing getDashboardWebhooks to explicitly select only the non-sensitive fields
used by the UI (e.g. id, url, name, isActive, createdAt, etc.) instead of
returning the whole record, and ensure any codepaths that currently expect a
Webhook type are updated so the client-side Webhook type does not include secret
(keep secret server-only and only expose it in tightly-scoped create/update
responses or admin endpoints). Locate the function getDashboardWebhooks and the
Webhook/UI type and add an explicit select projection or mapping to omit
WebhookEndpoint.secret, then update associated types and callers (e.g., the API
route GET /api/webhooks and initialWebhooks usage) to match the reduced shape.

In `@apps/cms/src/lib/queries/dashboard/usage.ts`:
- Around line 193-197: The media.totalSize value is incorrectly named because it
sums only the 10-record recentMediaUploads; change the field name to something
explicit (e.g. media.recentUploadsSize) where the usage object is constructed
(replace totalSize: recentMediaUploads.reduce(...) with recentUploadsSize: ...),
update any related type/interface for that usage object, and update the UI
consumer (the MediaUsageCard / media-usage-card component) to read
recentUploadsSize instead of totalSize so the label/prop accurately reflects
that this is the size of recent uploads rather than total storage.

---

Nitpick comments:
In `@apps/cms/src/lib/queries/dashboard/usage.ts`:
- Around line 18-156: The two sequential Promise.all batches cause an extra DB
round-trip; merge the first array (producing apiEvents, apiPrevPeriodCount,
apiTotalCount) with the second larger array (producing webhookTotal,
webhookWeek, webhookDay, webhookTopEndpoint, webhookEvents, mediaTotals,
mediaLast30, mediaLastUpload, recentMediaUploads) into a single Promise.all
call, then build chartBuckets and compute apiChart/apiLastPeriodCount/apiChange
after that single await; locate the arrays passed to Promise.all that populate
variables named apiEvents, apiPrevPeriodCount, apiTotalCount and the later
variables webhookTotal, webhookWeek, webhookDay, webhookTopEndpoint,
webhookEvents, mediaTotals, mediaLast30, mediaLastUpload, recentMediaUploads and
combine them into one concurrent request (ensuring your DB connection pool can
handle the increased concurrency).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78b92e79-75a7-4dbd-b948-d4409213efed

📥 Commits

Reviewing files that changed from the base of the PR and between 12a73f8 and 9a03aa1.

📒 Files selected for processing (40)
  • apps/cms/next.config.ts
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/media/loading.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/loading.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/fields/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/keys/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page.tsx
  • apps/cms/src/app/api/authors/route.ts
  • apps/cms/src/app/api/categories/route.ts
  • apps/cms/src/app/api/fields/route.ts
  • apps/cms/src/app/api/keys/route.ts
  • apps/cms/src/app/api/media/route.ts
  • apps/cms/src/app/api/metrics/usage/route.ts
  • apps/cms/src/app/api/posts/route.ts
  • apps/cms/src/app/api/tags/route.ts
  • apps/cms/src/app/api/webhooks/route.ts
  • apps/cms/src/components/categories/columns.tsx
  • apps/cms/src/components/keys/columns.tsx
  • apps/cms/src/components/layout/wrapper.tsx
  • apps/cms/src/components/nav/nav-main.tsx
  • apps/cms/src/components/nav/nav-settings.tsx
  • apps/cms/src/components/posts/columns.tsx
  • apps/cms/src/components/shared/page-loader.tsx
  • apps/cms/src/components/tags/columns.tsx
  • apps/cms/src/components/ui/activity-indicator.module.css
  • apps/cms/src/components/ui/activity-indicator.tsx
  • apps/cms/src/lib/queries/dashboard/authors.ts
  • apps/cms/src/lib/queries/dashboard/media.ts
  • apps/cms/src/lib/queries/dashboard/posts.ts
  • apps/cms/src/lib/queries/dashboard/settings.ts
  • apps/cms/src/lib/queries/dashboard/taxonomy.ts
  • apps/cms/src/lib/queries/dashboard/usage.ts
  • apps/cms/src/lib/queries/dashboard/workspace.ts
  • apps/cms/src/types/dashboard.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/cms/src/components/ui/activity-indicator.module.css
🚧 Files skipped from review as they are similar to previous changes (16)
  • apps/cms/src/app/api/tags/route.ts
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/media/loading.tsx
  • apps/cms/src/app/api/webhooks/route.ts
  • apps/cms/src/app/api/categories/route.ts
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page-client.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/(home)/page.tsx
  • apps/cms/src/app/api/metrics/usage/route.ts
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/posts/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/tags/page.tsx
  • apps/cms/src/app/api/fields/route.ts
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/keys/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/authors/page.tsx
  • apps/cms/src/app/api/media/route.ts
  • apps/cms/src/app/api/posts/route.ts
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/categories/page.tsx
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/media/page.tsx

Comment thread apps/cms/src/lib/queries/dashboard/posts.ts Outdated
Comment on lines +38 to +49
export async function getDashboardWebhooks(
workspaceId: string
): Promise<Webhook[]> {
return db.webhookEndpoint.findMany({
where: {
workspaceId,
},
orderBy: {
createdAt: "desc",
},
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1) Does the webhookEndpoint model expose a secret/signing field?
rg -nP -C2 'model\s+WebhookEndpoint' --glob '**/*.prisma'
rg -nP -C1 '\b(secret|signingSecret|signing_secret)\b' --glob '**/*.prisma'
# 2) What fields does the Webhook type declare?
fd -t f 'webhook.ts' -p 'types' --exec cat {}
# 3) Is the webhooks result passed straight to a client component?
fd -t f 'page-client.tsx' -p 'settings/webhooks' --exec rg -nP -C2 'initialWebhooks' {}

Repository: usemarble/marble

Length of output: 768


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate getDashboardWebhooks usage
rg -n "getDashboardWebhooks\s*\(" -S .

# Find where initialWebhooks is created/passed
rg -n "initialWebhooks" -S apps/cms/src

# Locate the client component mentioned in the original request
fd -t f "page-client.tsx" apps/cms/src --exec echo

# Locate webhook type definition
fd -t f "webhook.ts" . --exec wc -l {} \; --max-depth 6 2>/dev/null || true
fd -t f "*webhook*.*ts" apps/cms/src --max-depth 6 --exec echo {} \;

# If we can find a likely type file, print it (trimmed)
typefile="$(fd -t f "webhook.ts" . | head -n 1 || true)"
if [ -n "${typefile}" ]; then
  echo "=== Showing: $typefile ==="
  sed -n '1,200p' "$typefile"
fi

# Print the getDashboardWebhooks implementation context
sed -n '1,120p' apps/cms/src/lib/queries/dashboard/settings.ts

# If API route exists, inspect quickly (trimmed)
if [ -f "apps/cms/src/app/api/webhooks/route.ts" ]; then
  echo "=== Showing: apps/cms/src/app/api/webhooks/route.ts ==="
  sed -n '1,200p' apps/cms/src/app/api/webhooks/route.ts
fi

Repository: usemarble/marble

Length of output: 2978


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Focused check: confirm prisma schema has secret field and where webhookEndpoint is projected in code.
rg -n "model WebhookEndpoint|secret\s+String" packages/db/prisma/schema.prisma
rg -n "webhookEndpoint\.findMany\(" -S apps/cms/src/lib/queries/dashboard/settings.ts
rg -n "webhookEndpoint" -S apps/cms/src/lib/queries/dashboard/settings.ts

Repository: usemarble/marble

Length of output: 197


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== apps/cms/src/lib/queries/dashboard/settings.ts (getDashboardWebhooks) ==="
sed -n '1,120p' apps/cms/src/lib/queries/dashboard/settings.ts

echo "=== apps/cms/src/app/api/webhooks/route.ts ==="
sed -n '1,120p' apps/cms/src/app/api/webhooks/route.ts

echo "=== apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page.tsx ==="
sed -n '1,120p' "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page.tsx"

echo "=== apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page-client.tsx ==="
sed -n '1,220p' "apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page-client.tsx"

echo "=== apps/cms/src/types/webhook.ts ==="
sed -n '1,200p' apps/cms/src/types/webhook.ts

Repository: usemarble/marble

Length of output: 8334


Restrict webhook fields: WebhookEndpoint.secret is exposed to the client

packages/db/prisma/schema.prisma defines WebhookEndpoint.secret: String, and apps/cms/src/lib/queries/dashboard/settings.ts#getDashboardWebhooks returns db.webhookEndpoint.findMany(...) without select/omit, so secret is included in:

  • apps/cms/src/app/api/webhooks/route.ts (GET /api/webhooks returns it directly)
  • apps/cms/src/app/(main)/[workspace]/(dashboard)/settings/webhooks/page.tsx via initialWebhooks (and apps/cms/src/types/webhook.ts models secret on the Webhook type)

Add an explicit select projection (or map/redact) for only the non-sensitive fields needed by the UI, and update the Webhook UI type/client to not include secret (keep secret server-only, or only return it in narrowly authorized create/update flows).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cms/src/lib/queries/dashboard/settings.ts` around lines 38 - 49,
getDashboardWebhooks currently returns full WebhookEndpoint records (including
the sensitive WebhookEndpoint.secret) to the client; fix by changing
getDashboardWebhooks to explicitly select only the non-sensitive fields used by
the UI (e.g. id, url, name, isActive, createdAt, etc.) instead of returning the
whole record, and ensure any codepaths that currently expect a Webhook type are
updated so the client-side Webhook type does not include secret (keep secret
server-only and only expose it in tightly-scoped create/update responses or
admin endpoints). Locate the function getDashboardWebhooks and the Webhook/UI
type and add an explicit select projection or mapping to omit
WebhookEndpoint.secret, then update associated types and callers (e.g., the API
route GET /api/webhooks and initialWebhooks usage) to match the reduced shape.

Comment thread apps/cms/src/lib/queries/dashboard/usage.ts
Copy link
Copy Markdown
Member

@taqh taqh left a comment

Choose a reason for hiding this comment

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

Works

@taqh taqh disabled auto-merge May 30, 2026 13:22
@taqh taqh merged commit 5705db9 into main May 30, 2026
10 checks passed
@mezotv mezotv deleted the codex/dashboard-navigation-performance branch May 30, 2026 13:22
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.

2 participants