Skip to content

feat(web): seed permission sync banner with server-side initial state#942

Merged
brendan-kellam merged 6 commits intomainfrom
brendan/permission-sync-banner-improvements
Feb 26, 2026
Merged

feat(web): seed permission sync banner with server-side initial state#942
brendan-kellam merged 6 commits intomainfrom
brendan/permission-sync-banner-improvements

Conversation

@brendan-kellam
Copy link
Contributor

@brendan-kellam brendan-kellam commented Feb 26, 2026

Summary

  • Refactors getPermissionSyncStatus into a reusable exported server action so it can be called directly from the layout
  • Passes the server-fetched result as initialData to the banner's react-query, eliminating the loading flash on first render
  • Fixes hasPendingFirstSync to also consider accounts with no sync jobs yet as pending (handles the window between account creation and first job being scheduled)

Test plan

  • Verify the permission sync banner appears immediately on page load without a flash/delay
  • Verify the banner disappears once the first sync completes
  • Verify polling continues while sync is in progress

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Prevented the permission sync banner from briefly flashing on initial page load.
    • Ensured the banner appears correctly for accounts awaiting their first sync.
    • Banner now reliably reflects server-reported permission sync status on first render without requiring a page refresh.

Refactors getPermissionSyncStatus into a reusable server action and
passes the result as initialData to the banner query, eliminating the
loading flash on first render. Also fixes hasPendingFirstSync to treat
accounts with no sync jobs yet as pending.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

This comment has been minimized.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 26, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b96e4bd and 93e7865.

📒 Files selected for processing (1)
  • CHANGELOG.md

Walkthrough

Extracts server-side helper getPermissionSyncStatus to compute whether a first-time permission sync is pending; layout calls it during server render and passes the result as an initialHasPendingFirstSync prop to PermissionSyncBanner to avoid flash/late-appearance. Entitlement and experiment gating remain applied.

Changes

Cohort / File(s) Summary
Changelog
CHANGELOG.md
Adds two Unreleased entries documenting fixes for permission sync banner flashing and delayed appearance.
Client — Banner & Layout
packages/web/src/app/[domain]/components/permissionSyncBanner.tsx, packages/web/src/app/[domain]/layout.tsx
PermissionSyncBanner now accepts initialHasPendingFirstSync prop; layout fetches permission sync status server-side and passes initial boolean into banner render path to prevent flashing/delayed appearance.
Server — Permission Sync Status API
packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts, packages/web/src/app/api/(server)/ee/permissionSyncStatus/route.ts
Adds getPermissionSyncStatus server helper exporting PermissionSyncStatusResponse and encapsulating auth/entitlement checks, experiment gating, DB query for accounts + latest permissionSyncJobs, and pending-first-sync logic; route GET now delegates to this helper and uses service error handling.
Client Imports / Typing
packages/web/src/app/api/(client)/client.ts, package.json
Updated import to reference the new server API module for PermissionSyncStatusResponse (minor import path change).

Sequence Diagram(s)

sequenceDiagram
  participant Layout as Server Layout
  participant Helper as getPermissionSyncStatus
  participant DB as Database
  participant Banner as PermissionSyncBanner (client)

  Layout->>Helper: call getPermissionSyncStatus (server-side, with auth)
  Helper->>Helper: verify entitlements & experiment flag
  Helper->>DB: query accounts + latest permissionSyncJobs
  DB-->>Helper: accounts + jobs
  Helper-->>Layout: { hasPendingFirstSync: boolean }
  Layout->>Banner: render with initialHasPendingFirstSync prop
  Note right of Banner: Client initializes banner state from prop to avoid flash
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • msukkari
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: seeding the permission sync banner with server-side initial state to prevent loading flashes.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch brendan/permission-sync-banner-improvements

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.

brendan-kellam and others added 2 commits February 25, 2026 16:45
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consolidate permission sync banner fixes for clarity.
Copy link
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.

🧹 Nitpick comments (1)
packages/web/src/app/api/(server)/ee/permissionSyncStatus/route.ts (1)

62-69: Simplify the pending-sync predicate for readability.

Line 68 repeats the permissionSyncJobs.length > 0 check; extracting the latest job makes the condition easier to scan without changing behavior.

♻️ Suggested simplification
-        const hasPendingFirstSync = env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' &&
-            accounts.some(account =>
-                account.permissionSyncedAt === null &&
-                // `@note`: to handle the case where the permission sync job
-                // has not yet been scheduled for a new account, we consider
-                // accounts with no permission sync jobs as having a pending first sync.
-                (account.permissionSyncJobs.length === 0 || (account.permissionSyncJobs.length > 0 && activeStatuses.includes(account.permissionSyncJobs[0].status)))
-            )
+        const hasPendingFirstSync =
+            env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' &&
+            accounts.some((account) => {
+                const latestSyncJob = account.permissionSyncJobs[0];
+                return (
+                    account.permissionSyncedAt === null &&
+                    // `@note`: to handle the case where the permission sync job
+                    // has not yet been scheduled for a new account, we consider
+                    // accounts with no permission sync jobs as having a pending first sync.
+                    (latestSyncJob === undefined || activeStatuses.includes(latestSyncJob.status))
+                );
+            });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/api/`(server)/ee/permissionSyncStatus/route.ts around
lines 62 - 69, The predicate computing hasPendingFirstSync is verbose and
repeats permissionSyncJobs.length > 0; refactor to extract the latest job once
for clarity: inside the accounts.some callback, get the first job (e.g., const
latestJob = account.permissionSyncJobs[0]) and then check
account.permissionSyncedAt === null && (account.permissionSyncJobs.length === 0
|| activeStatuses.includes(latestJob.status)); keep the outer feature flag check
(env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true') and the referenced
symbols hasPendingFirstSync, accounts, permissionSyncedAt, permissionSyncJobs,
and activeStatuses to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/web/src/app/api/`(server)/ee/permissionSyncStatus/route.ts:
- Around line 62-69: The predicate computing hasPendingFirstSync is verbose and
repeats permissionSyncJobs.length > 0; refactor to extract the latest job once
for clarity: inside the accounts.some callback, get the first job (e.g., const
latestJob = account.permissionSyncJobs[0]) and then check
account.permissionSyncedAt === null && (account.permissionSyncJobs.length === 0
|| activeStatuses.includes(latestJob.status)); keep the outer feature flag check
(env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true') and the referenced
symbols hasPendingFirstSync, accounts, permissionSyncedAt, permissionSyncJobs,
and activeStatuses to locate the change.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 564480d and 892dde5.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • packages/web/src/app/[domain]/components/permissionSyncBanner.tsx
  • packages/web/src/app/[domain]/layout.tsx
  • packages/web/src/app/api/(server)/ee/permissionSyncStatus/route.ts

…route export

Next.js does not allow exporting arbitrary functions from route.ts files.
Extracts the getPermissionSyncStatus helper into a sibling api.ts module
and updates route.ts and layout.tsx imports accordingly.

Co-authored-by: Brendan Kellam <brendan-kellam@users.noreply.github.com>
Copy link
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.

🧹 Nitpick comments (1)
packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts (1)

31-56: Short-circuit the DB read when the experiment is disabled.

prisma.account.findMany still runs even when EXPERIMENT_EE_PERMISSION_SYNC_ENABLED !== 'true'. Since this helper is now used during layout render, this adds avoidable request-time load.

♻️ Proposed refactor
 export const getPermissionSyncStatus = async (): Promise<PermissionSyncStatusResponse | ServiceError> => sew(async () =>
     withAuthV2(async ({ prisma, user }) => {
         const entitlements = getEntitlements();
         if (!entitlements.includes('permission-syncing')) {
             return {
                 statusCode: StatusCodes.FORBIDDEN,
                 errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS,
                 message: "Permission syncing is not enabled for your license",
             } satisfies ServiceError;
         }

+        if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED !== 'true') {
+            return { hasPendingFirstSync: false } satisfies PermissionSyncStatusResponse;
+        }
+
         const accounts = await prisma.account.findMany({
             where: {
                 userId: user.id,
                 provider: { in: ['github', 'gitlab', 'bitbucket-cloud', 'bitbucket-server'] }
             },
@@
-        const hasPendingFirstSync = env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' &&
-            accounts.some(account =>
+        const hasPendingFirstSync = accounts.some(account =>
                 account.permissionSyncedAt === null &&
                 // `@note`: to handle the case where the permission sync job
                 // has not yet been scheduled for a new account, we consider
                 // accounts with no permission sync jobs as having a pending first sync.
                 (account.permissionSyncJobs.length === 0 || (account.permissionSyncJobs.length > 0 && activeStatuses.includes(account.permissionSyncJobs[0].status)))
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/web/src/app/api/`(server)/ee/permissionSyncStatus/api.ts around
lines 31 - 56, The code always calls prisma.account.findMany even when the
experiment flag is off; move the env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED ===
'true' check before the database call and short-circuit (return false / no
pending) when the flag is not set, so that prisma.account.findMany and
subsequent logic (the hasPendingFirstSync computation and use of
account.permissionSyncJobs) are skipped unless the experiment is enabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/web/src/app/api/`(server)/ee/permissionSyncStatus/api.ts:
- Around line 31-56: The code always calls prisma.account.findMany even when the
experiment flag is off; move the env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED ===
'true' check before the database call and short-circuit (return false / no
pending) when the flag is not set, so that prisma.account.findMany and
subsequent logic (the hasPendingFirstSync computation and use of
account.permissionSyncJobs) are skipped unless the experiment is enabled.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1aa3805 and e2083c3.

📒 Files selected for processing (3)
  • packages/web/src/app/[domain]/layout.tsx
  • packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts
  • packages/web/src/app/api/(server)/ee/permissionSyncStatus/route.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/web/src/app/[domain]/layout.tsx

@brendan-kellam
Copy link
Contributor Author

brendan-kellam commented Feb 26, 2026

@claude

#57 176.8 Type error: Module '"../(server)/ee/permissionSyncStatus/route"' has no exported member 'PermissionSyncStatusResponse'.
#57 176.8 
#57 176.8   20 |     FileSourceResponse,
#57 176.8   21 | } from "@/features/git";
#57 176.8 > 22 | import { PermissionSyncStatusResponse } from "../(server)/ee/permissionSyncStatus/route";

@claude
Copy link

claude bot commented Feb 26, 2026

Claude finished @brendan-kellam's task in 3m 4s —— View job


Tasks

  • Gather context
  • Read relevant files
  • Apply CodeRabbit nitpick improvements to api.ts
  • Push changes — ⚠️ blocked (need git fetch/git pull approval)

What was done

Applied both CodeRabbit suggestions to packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts:

  1. Short-circuit DB read — added an early return when EXPERIMENT_EE_PERMISSION_SYNC_ENABLED !== 'true', avoiding an unnecessary prisma.account.findMany call during every layout render when the experiment is off.

  2. Simplified predicate — extracted latestSyncJob = account.permissionSyncJobs[0] and replaced the redundant length > 0 double-check with latestSyncJob === undefined.

The changes are committed locally as 8c18d6f but the push is failing because the remote has a commit that isn't in my local checkout (git fetch and git pull require explicit approval here). If you can approve those commands or push the diff below manually, the PR will be up to date.

Diff to apply
--- a/packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts
+++ b/packages/web/src/app/api/(server)/ee/permissionSyncStatus/api.ts
@@ -28,17 +28,22 @@ export const getPermissionSyncStatus = ...
         }
 
+        if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED !== 'true') {
+            return { hasPendingFirstSync: false } satisfies PermissionSyncStatusResponse;
+        }
+
         const accounts = await prisma.account.findMany({
             where: { ... },
             include: { permissionSyncJobs: { orderBy: { createdAt: 'desc' }, take: 1 } }
         });
 
         const activeStatuses: AccountPermissionSyncJobStatus[] = [...];
 
-        const hasPendingFirstSync = env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' &&
-            accounts.some(account =>
-                account.permissionSyncedAt === null &&
-                (account.permissionSyncJobs.length === 0 || (account.permissionSyncJobs.length > 0 && activeStatuses.includes(account.permissionSyncJobs[0].status)))
-            )
+        const hasPendingFirstSync = accounts.some((account) => {
+            const latestSyncJob = account.permissionSyncJobs[0];
+            return (
+                account.permissionSyncedAt === null &&
+                (latestSyncJob === undefined || activeStatuses.includes(latestSyncJob.status))
+            );
+        });

@claude
Copy link

claude bot commented Feb 26, 2026

Claude finished @brendan-kellam's task in 38s —— View job


Tasks

  • Gather context
  • Find the file with the incorrect import
  • Fix the import to use api.ts instead of route.ts
  • Commit and push changes

Fixed! packages/web/src/app/api/(client)/client.ts line 22 was still importing PermissionSyncStatusResponse from route instead of the newly created api.ts. Updated the import path.

@brendan-kellam brendan-kellam merged commit 7d2d4bb into main Feb 26, 2026
7 checks passed
@brendan-kellam brendan-kellam deleted the brendan/permission-sync-banner-improvements branch February 26, 2026 01:52
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