Skip to content

[Dashboard][Backend] - Internal support tooling#1135

Closed
madster456 wants to merge 6 commits intodevfrom
dashboard/internal-support
Closed

[Dashboard][Backend] - Internal support tooling#1135
madster456 wants to merge 6 commits intodevfrom
dashboard/internal-support

Conversation

@madster456
Copy link
Copy Markdown
Collaborator

@madster456 madster456 commented Jan 27, 2026

Summary by CodeRabbit

Release Notes

  • New Features
    • Added comprehensive internal support dashboard allowing support staff to browse and inspect project configurations, user accounts, team structures, and system events.
    • Introduced internal support API endpoints with team membership authentication and paginated queries for efficient data retrieval.
    • Added environment configuration for internal support team identification and access control.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Copy Markdown

vercel Bot commented Jan 27, 2026

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

Project Deployment Review Updated (UTC)
stack-backend Ready Ready Preview, Comment Jan 27, 2026 7:17am
stack-dashboard Ready Ready Preview, Comment Jan 27, 2026 7:17am
stack-demo Ready Ready Preview, Comment Jan 27, 2026 7:17am
stack-docs Ready Ready Preview, Comment Jan 27, 2026 7:17am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 27, 2026

Warning

Rate limit exceeded

@madster456 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 22 minutes and 28 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR introduces a complete internal support dashboard system for Stack Auth. It adds backend API routes for querying projects, teams, users, and events filtered by internal support team membership, along with a comprehensive frontend dashboard for browsing and inspecting this data interactively.

Changes

Cohort / File(s) Summary
Environment Configuration
apps/backend/.env.development
Added STACK_INTERNAL_SUPPORT_TEAM_ID environment variable for support team identification.
Support Authentication Infrastructure
apps/backend/src/app/api/latest/internal/support/support-auth.ts
Introduced support auth module with SUPPORT_TEAM_ID constant, supportAuthSchema validation schema, SupportAuth type, and validateSupportTeamMembership() function enforcing membership checks against internal tenancy.
Internal Support API Endpoints
apps/backend/src/app/api/latest/internal/support/projects/route.tsx, projects/[projectId]/teams/route.tsx, projects/[projectId]/users/route.tsx, projects/[projectId]/events/route.tsx
Four new GET endpoint handlers exposing paginated listings of projects, teams, users, and events with search, filtering, and membership validation. Each endpoint resolves tenancy, queries via Prisma, applies pagination limits, and returns structured JSON responses with item counts.
Dashboard UI
apps/dashboard/src/app/(main)/(protected)/internal-support/page.tsx, page-client.tsx
Implemented full-featured internal support dashboard with two-panel resizable layout, search functionality, project/user/team/event detail views, config rendering, and interactive cross-linking between entities. Includes type contracts, state management, debounced queries, and loading placeholders.

Sequence Diagram

sequenceDiagram
    actor User
    participant Dashboard as Dashboard<br/>(Browser)
    participant API as Backend API
    participant Auth as Support Auth<br/>Module
    participant DB as Tenancy<br/>Prisma

    User->>Dashboard: Open Internal Support Dashboard
    Dashboard->>API: GET /projects with auth headers
    API->>Auth: validateSupportTeamMembership(auth)
    Auth->>DB: Lookup user in internal support team
    DB-->>Auth: User member status
    alt User is support member
        Auth-->>API: Return user + tenancy
        API->>DB: Query projects with metadata
        DB-->>API: Project list + counts
        API-->>Dashboard: JSON projects response
        Dashboard->>User: Render project list
    else User not authorized
        Auth-->>API: 403 Forbidden
        API-->>Dashboard: Error response
        Dashboard->>User: Show error toast
    end
    User->>Dashboard: Click project → view teams/users/events
    Dashboard->>API: GET /projects/[id]/teams|users|events
    API->>Auth: Validate membership (cached)
    Auth-->>API: Authorized
    API->>DB: Query filtered data
    DB-->>API: Results + pagination
    API-->>Dashboard: Structured response
    Dashboard->>User: Render detail panel
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A dashboard blooms for support so fine,
Where teams and users in panels align,
With tenancy guards and permission gates,
We peek inside what Stack generates! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided. The template only contains a reference to CONTRIBUTING.md guidelines but no substantive content about changes, rationale, or testing. Add a detailed description explaining the purpose of the internal support tooling, key features added, and any testing or validation performed.
Docstring Coverage ⚠️ Warning Docstring coverage is 23.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title '[Dashboard][Backend] - Internal support tooling' clearly summarizes the main change: addition of internal support tooling across dashboard and backend components.

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


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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jan 27, 2026

Greptile Overview

Greptile Summary

Implements internal support tooling to allow Stack Auth support team members to view and manage customer projects. Adds secure backend API endpoints protected by team membership validation and a comprehensive dashboard UI with dual-panel navigation.

Key Changes

  • Backend API: Created 5 new support endpoints under /api/v1/internal/support for listing projects, users, teams, and events with proper authorization via STACK_INTERNAL_SUPPORT_TEAM_ID environment variable
  • Authorization: Implemented team-based access control requiring membership in the configured support team within the "internal" project
  • Dashboard UI: Built a sophisticated dual-panel interface with search, filtering, and detailed views for projects, users, teams, and events
  • Configuration: Added STACK_INTERNAL_SUPPORT_TEAM_ID environment variable to .env.development

Implementation Quality

  • All API endpoints use SmartRouteHandler with proper request/response schemas
  • Authorization is consistently validated at the start of each handler using validateSupportTeamMembership
  • Client code properly uses runAsynchronouslyWithAlert for async operations
  • Error handling is comprehensive with appropriate user feedback via toasts
  • UI follows existing patterns with resizable panels, search/filter capabilities, and metadata display

Confidence Score: 5/5

  • This PR is safe to merge with proper authorization controls and well-structured code
  • The implementation demonstrates high quality with proper security controls, consistent use of established patterns (SmartRouteHandler, runAsynchronouslyWithAlert), comprehensive error handling, and clean separation of concerns between backend and frontend
  • No files require special attention

Important Files Changed

Filename Overview
apps/backend/src/app/api/latest/internal/support/support-auth.ts Authentication and authorization module for internal support endpoints with proper team membership validation
apps/backend/src/app/api/latest/internal/support/projects/route.tsx Project listing endpoint with search, pagination, and comprehensive project details aggregation
apps/backend/src/app/api/latest/internal/support/projects/[projectId]/users/route.tsx User listing endpoint for projects with search, filtering by userId, and comprehensive user details
apps/backend/src/app/api/latest/internal/support/projects/[projectId]/teams/route.tsx Team listing endpoint for projects with search, filtering by teamId, and member previews
apps/dashboard/src/app/(main)/(protected)/internal-support/page-client.tsx Comprehensive client-side support dashboard with dual-panel UI, search, and detailed views for projects, users, teams, and events

Sequence Diagram

sequenceDiagram
    participant User as Support Agent
    participant Dashboard as Internal Support Dashboard
    participant Backend as Support API (/api/v1/internal/support)
    participant AuthModule as Support Authorization
    participant DB as Database (Prisma)
    
    User->>Dashboard: Access /internal-support
    Dashboard->>Dashboard: useAuthHeaders() - Get authentication token
    
    Note over Dashboard,Backend: Search for Projects
    Dashboard->>Backend: GET /projects?search=query
    Backend->>AuthModule: validateSupportTeamMembership()
    AuthModule->>DB: Check team membership in "internal" project
    DB-->>AuthModule: Return membership status
    alt Not a support team member
        AuthModule-->>Backend: Throw 403 Error
        Backend-->>Dashboard: 403 Forbidden
    else Is support team member
        AuthModule-->>Backend: Return validated authorization
        Backend->>DB: Query projects with filters
        DB-->>Backend: Return projects list
        Backend->>DB: Fetch additional project details (config, counts)
        DB-->>Backend: Return enriched project data
        Backend-->>Dashboard: Return projects with metadata
        Dashboard->>Dashboard: Display project cards
    end
    
    Note over Dashboard,Backend: View Project Details
    User->>Dashboard: Click on project
    Dashboard->>Backend: GET /projects/{projectId}/users
    Backend->>AuthModule: validateSupportTeamMembership()
    AuthModule->>DB: Verify team membership
    AuthModule-->>Backend: Return validated authorization
    Backend->>DB: Get tenancy for projectId
    Backend->>DB: Query users with filters
    DB-->>Backend: Return users with authentication methods and teams
    Backend-->>Dashboard: Return user list
    
    Dashboard->>Backend: GET /projects/{projectId}/teams
    Backend->>AuthModule: validateSupportTeamMembership()
    AuthModule-->>Backend: Return validated authorization
    Backend->>DB: Query teams with member previews
    DB-->>Backend: Return teams list
    Backend-->>Dashboard: Return team list
    
    Dashboard->>Backend: GET /projects/{projectId}/events
    Backend->>AuthModule: validateSupportTeamMembership()
    AuthModule-->>Backend: Return validated authorization
    Backend->>DB: Query events by projectId and branchId
    DB-->>Backend: Return events with IP info
    Backend-->>Dashboard: Return events list
    
    Dashboard->>Dashboard: Display project details in tabs
    
    Note over Dashboard,Backend: Navigate to User or Team Details
    User->>Dashboard: Click on user or team
    Dashboard->>Backend: GET /projects/{projectId}/users?userId={id}
    Backend->>AuthModule: validateSupportTeamMembership()
    AuthModule-->>Backend: Return validated authorization
    Backend->>DB: Query specific user
    DB-->>Backend: Return user details
    Backend-->>Dashboard: Return user
    Dashboard->>Dashboard: Display in right panel
Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

No files reviewed, no comments

Edit Code Review Agent Settings | Greptile

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: 11

Caution

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

⚠️ Outside diff range comments (1)
apps/backend/.env.development (1)

52-57: Fix dotenv-linter key ordering.
The linter expects STACK_INTERNAL_SUPPORT_TEAM_ID before STACK_OPENAI_API_KEY; reordering keeps env lint clean.

🧹 Proposed reorder
-STACK_FREESTYLE_API_KEY=mock_stack_freestyle_key
-STACK_OPENAI_API_KEY=mock_openai_api_key
-STACK_STRIPE_SECRET_KEY=sk_test_mockstripekey
-STACK_STRIPE_WEBHOOK_SECRET=mock_stripe_webhook_secret
-STACK_INTERNAL_SUPPORT_TEAM_ID=mock_internal_support_team_id
+STACK_FREESTYLE_API_KEY=mock_stack_freestyle_key
+STACK_INTERNAL_SUPPORT_TEAM_ID=mock_internal_support_team_id
+STACK_OPENAI_API_KEY=mock_openai_api_key
+STACK_STRIPE_SECRET_KEY=sk_test_mockstripekey
+STACK_STRIPE_WEBHOOK_SECRET=mock_stripe_webhook_secret
🤖 Fix all issues with AI agents
In
`@apps/backend/src/app/api/latest/internal/support/projects/`[projectId]/events/route.tsx:
- Around line 37-38: The current parsing of query params uses parseInt on
req.query.limit and req.query.offset which can return NaN or negative values;
update the logic around limit and offset (the variables `limit` and `offset` in
route.tsx) to validate and clamp results: parse the values with base 10, default
to safe numbers when parseInt yields NaN, clamp `limit` to a min of 1 and max of
100, and clamp `offset` to a min of 0; then pass these validated `limit` (for
Prisma take) and `offset` (for Prisma skip) into the Prisma query to avoid
invalid/skipped queries.
- Around line 33-35: Replace the non-null assertion on fullReq.auth in the
handler with an explicit guard: before calling validateSupportTeamMembership,
check if fullReq.auth is present and if not throw a clear, descriptive error
(e.g., "Missing authentication on request" or an appropriate HTTP error) so
validateSupportTeamMembership(fullReq.auth) is only called with a defined auth
object; update the handler function accordingly to reference fullReq.auth
without using "!".

In
`@apps/backend/src/app/api/latest/internal/support/projects/`[projectId]/teams/route.tsx:
- Around line 35-37: Replace the non-null assertion on fullReq.auth passed to
validateSupportTeamMembership with an explicit defensive guard: assign or inline
using fullReq.auth ?? throwErr('Missing auth in support team route') so that if
auth is undefined an explicit error is thrown; update the call site (handler:
async (req, fullReq) => { await validateSupportTeamMembership(fullReq.auth!); })
to call validateSupportTeamMembership(auth) where auth is the guarded value and
ensure throwErr is imported/available and the error message clearly states the
missing auth and route context.
- Around line 41-42: The current parsing of query params uses parseInt directly
and Math.min which can produce NaN or negative values; update the logic around
limit and offset parsing in route.tsx to: parse each param into a number, check
Number.isFinite and not NaN, clamp limit to the range [1,100] (or default to 25)
and clamp offset to >=0 (default 0); replace the existing const limit and const
offset assignments with these validated/clamped values so downstream pagination
never receives NaN or negative numbers.

In
`@apps/backend/src/app/api/latest/internal/support/projects/`[projectId]/users/route.tsx:
- Around line 41-42: The limit and offset parsing in route.tsx currently uses
parseInt directly which can yield NaN or negative values; change the parsing for
req.query.limit and req.query.offset to validate and clamp results: parse the
raw value, if it is NaN fall back to defaults (limit default 25, offset default
0), ensure limit is clamped to the range [1,100] (e.g., Math.max(1,
Math.min(100, parsed))) and ensure offset is >= 0 (e.g., Math.max(0, parsed));
update the variables named limit and offset (or extract a small helper like
parsePositiveIntOrDefault used by both) so Prisma queries never receive NaN or
negative pagination values.
- Around line 35-37: The handler currently calls
validateSupportTeamMembership(fullReq.auth!) using a non-null assertion; replace
this with an explicit guard that checks fullReq.auth and throws a clear error if
missing before calling validateSupportTeamMembership. Locate the async handler
function in route.tsx and add an auth presence check (e.g., if (!fullReq.auth)
throw new Error("Missing authentication in request") or a typed HttpError), then
pass fullReq.auth to validateSupportTeamMembership(fullReq.auth); ensure
subsequent code relies on the validated/authenticated value rather than assuming
non-null.

In `@apps/backend/src/app/api/latest/internal/support/projects/route.tsx`:
- Around line 33-35: Replace the non-null assertion on fullReq.auth with an
explicit guard: obtain auth via nullish coalescing (e.g. const auth =
fullReq.auth ?? throwErr("...")) and pass that auth into
validateSupportTeamMembership instead of fullReq.auth!; update the handler so it
throws a clear error message when auth is missing (using your existing throwErr
helper) before calling validateSupportTeamMembership.
- Around line 82-97: Replace the bare try-catch blocks that call
getSoleTenancyFromProjectBranch with the function's returnNullIfNotFound
behavior: call getSoleTenancyFromProjectBranch(project.id, DEFAULT_BRANCH_ID,
true) for the config fetch (where fullConfig is set via
renderedOrganizationConfigToProjectCrud) and for the owner team lookup, remove
the surrounding try-catch and check for a null tenancy result before accessing
tenancy.config or creating the prisma client; this avoids swallowing real errors
and preserves existing null-handling logic when tenancy is not found.
- Around line 38-39: The limit/offset parsing in route.tsx can produce NaN or
negative values; update the parsing of req.query.limit and req.query.offset (the
const limit and offset variables) to coerce to integers safely, ensure they are
finite and >= 0, and fall back to defaults (limit=25, offset=0) when invalid;
additionally clamp limit to a maximum of 100 before passing to Prisma's take and
skip (use safe checks like Number.isFinite after Number(...) or parseInt,
Math.max(0, ...) and Math.min(100, ...) to enforce bounds and avoid passing
invalid values to Prisma).

In `@apps/backend/src/app/api/latest/internal/support/support-auth.ts`:
- Line 18: The module-level call to getEnvVariable for SUPPORT_TEAM_ID causes an
import-time throw if the env var is missing; change the declaration of
SUPPORT_TEAM_ID to call getEnvVariable("STACK_INTERNAL_SUPPORT_TEAM_ID", "") (or
another safe default like an empty string) so the module does not crash at
import, and ensure the existing runtime authorization check that performs the
403 validation still verifies that SUPPORT_TEAM_ID is present/valid before
granting access.

In `@apps/dashboard/src/app/`(main)/(protected)/internal-support/page-client.tsx:
- Around line 1042-1046: Remove the debug JSX block that prints internal details
(the <div> containing "Debug - project.id" and "Debug - config") from
page-client.tsx, or wrap it so it only renders in development (e.g., guard with
NODE_ENV or an isDev flag); locate the snippet that references project.id and
config and either delete those two inner <div> elements or conditionally render
them so they are not exposed in production builds.
🧹 Nitpick comments (7)
apps/backend/src/app/api/latest/internal/support/projects/route.tsx (1)

99-120: Cache internal tenancy/prisma for owner team lookup.
getSoleTenancyFromProjectBranch("internal", ...) and its Prisma client are fetched per project; prefetch once to avoid N+1 overhead.

⚡️ Prefetch internal tenancy/prisma
@@
-    // Fetch full details for each project
+    const internalTenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID, true);
+    const internalPrisma = internalTenancy ? await getPrismaClientForTenancy(internalTenancy) : null;
+
+    // Fetch full details for each project
@@
-        let ownerTeam = null;
-        if (project.ownerTeamId) {
-          try {
-            const internalTenancy = await getSoleTenancyFromProjectBranch("internal", DEFAULT_BRANCH_ID);
-            const internalPrisma = await getPrismaClientForTenancy(internalTenancy);
-            const team = await internalPrisma.team.findFirst({
-              where: {
-                tenancyId: internalTenancy.id,
-                teamId: project.ownerTeamId,
-              },
-            });
-            if (team) {
-              ownerTeam = {
-                id: team.teamId,
-                displayName: team.displayName,
-              };
-            }
-          } catch {
-            // Ignore errors
-          }
-        }
+        let ownerTeam = null;
+        if (project.ownerTeamId && internalPrisma && internalTenancy) {
+          const team = await internalPrisma.team.findFirst({
+            where: {
+              tenancyId: internalTenancy.id,
+              teamId: project.ownerTeamId,
+            },
+          });
+          if (team) {
+            ownerTeam = {
+              id: team.teamId,
+              displayName: team.displayName,
+            };
+          }
+        }
apps/backend/src/app/api/latest/internal/support/support-auth.ts (1)

38-41: Prefer an interface for SupportAuth.
Project style favors interface for object shapes. As per coding guidelines, prefer interfaces over type aliases for object shapes.

♻️ Suggested refactor
-export type SupportAuth = {
-  user: UsersCrud["Admin"]["Read"],
-  tenancy: Tenancy,
-};
+export interface SupportAuth {
+  user: UsersCrud["Admin"]["Read"];
+  tenancy: Tenancy;
+}
apps/dashboard/src/app/(main)/(protected)/internal-support/page-client.tsx (5)

50-97: Prefer interface over type for object shapes.

Per coding guidelines, SupportProject, SupportUser, SupportTeam, and SupportEvent should be declared as interfaces. The PanelContent union type (lines 119-124) is fine as a type since unions cannot be expressed with interfaces.

♻️ Suggested refactor
-type SupportProject = {
+interface SupportProject {
   id: string,
   // ... rest of properties
-};
+}

-type SupportUser = {
+interface SupportUser {
   id: string,
   // ... rest of properties
-};
+}

-type SupportTeam = {
+interface SupportTeam {
   id: string,
   // ... rest of properties
-};
+}

-type SupportEvent = {
+interface SupportEvent {
   id: string,
   // ... rest of properties
-};
+}

136-138: Consider logging or handling localStorage parse errors.

The empty catch block silently swallows JSON parsing errors, which could hide corrupted localStorage data. Consider at minimum logging to console or clearing the corrupted value.

♻️ Suggested improvement
     } catch {
-      // If error, use initial value
+      // Clear corrupted data and use initial value
+      console.warn(`Failed to parse localStorage key "${key}", using default value`);
+      window.localStorage.removeItem(key);
     }

190-193: Unused setWidgetConfig variable.

setWidgetConfig is declared but never used. Either remove it or implement the intended widget configuration feature.

♻️ Suggested fix
-  const [, setWidgetConfig] = useLocalStorage<string[]>(
+  const [] = useLocalStorage<string[]>(
     'support-dashboard-widgets',
     ['recent-projects', 'search']
   );

Or remove the hook entirely if widget configuration is not needed.


479-490: Consider consolidating search effects.

The initial load effect (lines 480-482) has an empty dependency array but references performSearch. Additionally, the debounced effect (lines 485-490) will also trigger a search on mount after 300ms. This causes two searches on initial mount.

♻️ Suggested consolidation

Remove the initial load effect and let the debounced effect handle the initial search:

-  // Initial load
-  useEffect(() => {
-    runAsynchronouslyWithAlert(performSearch());
-  }, []);
-
   // Debounced search
   useEffect(() => {
     const timer = setTimeout(() => {
       runAsynchronouslyWithAlert(performSearch());
     }, 300);
     return () => clearTimeout(timer);
   }, [searchQuery, searchType, performSearch]);

This ensures a single search on mount with consistent behavior.


1272-1296: Redundant error handling - let runAsynchronouslyWithAlert handle errors.

handleTeamClick has its own try-catch, but callers also wrap it with runAsynchronouslyWithAlert (line 1347). This causes double error handling and the internal catch swallows errors before runAsynchronouslyWithAlert can process them. Per guidelines, avoid try-catch-all patterns.

♻️ Suggested refactor
   const handleTeamClick = async (teamIdToFind: string) => {
     setIsLoadingTeam(teamIdToFind);
-    try {
-      const headers = await getHeaders();
-      const response = await fetch(
-        `${baseApiUrl}/api/v1/internal/support/projects/${projectId}/teams?teamId=${teamIdToFind}`,
-        { headers }
-      );
-      if (response.ok) {
-        const data = await response.json();
-        const team = data.items[0];
-        if (team) {
-          onSelectTeam(team);
-        } else {
-          toast({ variant: "destructive", title: "Team not found" });
-        }
+    const headers = await getHeaders();
+    const response = await fetch(
+      `${baseApiUrl}/api/v1/internal/support/projects/${projectId}/teams?teamId=${teamIdToFind}`,
+      { headers }
+    );
+    if (response.ok) {
+      const data = await response.json();
+      const team = data.items[0];
+      if (team) {
+        onSelectTeam(team);
       } else {
         toast({ variant: "destructive", title: "Failed to fetch team details" });
       }
-    } catch {
-      toast({ variant: "destructive", title: "Failed to fetch team details" });
-    } finally {
-      setIsLoadingTeam(null);
+    } else {
+      throw new Error("Failed to fetch team details");
     }
+    setIsLoadingTeam(null);
   };

Note: You'll need to ensure setIsLoadingTeam(null) is called even on error - consider using a finally-like pattern within runAsynchronouslyWithAlert or restructuring.

Comment thread apps/backend/src/app/api/latest/internal/support/projects/route.tsx
Comment thread apps/backend/src/app/api/latest/internal/support/projects/route.tsx Outdated
Comment thread apps/backend/src/app/api/latest/internal/support/projects/route.tsx Outdated
Comment thread apps/backend/src/app/api/latest/internal/support/support-auth.ts Outdated
Comment thread apps/dashboard/src/app/(main)/(protected)/internal-support/page-client.tsx Outdated
Comment on lines +446 to +490
const performSearch = useCallback(async () => {
setIsLoading(true);
try {
const headers = await getHeaders();
const params = new URLSearchParams({ limit: '50' });

if (searchQuery.trim()) {
if (searchType === 'id') {
params.set('projectId', searchQuery.trim());
} else {
params.set('search', searchQuery.trim());
}
}

const response = await fetch(`${baseApiUrl}/api/v1/internal/support/projects?${params}`, {
headers,
});

if (!response.ok) {
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
throw new Error(error.error || 'Failed to fetch projects');
}

const data = await response.json();
setSearchResults(data.items);
setTotal(data.total);
} catch (e) {
toast({ variant: "destructive", title: e instanceof Error ? e.message : "Search failed" });
} finally {
setIsLoading(false);
}
}, [getHeaders, baseApiUrl, searchQuery, searchType]);

// Initial load
useEffect(() => {
runAsynchronouslyWithAlert(performSearch());
}, []);

// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
runAsynchronouslyWithAlert(performSearch());
}, 300);
return () => clearTimeout(timer);
}, [searchQuery, searchType, performSearch]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Multiple API error handlers don't clear or reset state when fetches fail, causing stale data to be displayed alongside error toasts. This confuses users into thinking the old data is current.

View Details
📝 Patch Details
diff --git a/apps/dashboard/src/app/(main)/(protected)/internal-support/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/internal-support/page-client.tsx
index a7e30d60..e17a7ea6 100644
--- a/apps/dashboard/src/app/(main)/(protected)/internal-support/page-client.tsx
+++ b/apps/dashboard/src/app/(main)/(protected)/internal-support/page-client.tsx
@@ -470,6 +470,8 @@ function SearchPanel({
       setSearchResults(data.items);
       setTotal(data.total);
     } catch (e) {
+      setSearchResults([]);
+      setTotal(0);
       toast({ variant: "destructive", title: e instanceof Error ? e.message : "Search failed" });
     } finally {
       setIsLoading(false);
@@ -649,6 +651,7 @@ function ProjectDetailPanel({
         } else {
           const error = await response.json().catch(() => ({ error: response.statusText }));
           console.error('Failed to fetch owner team:', error);
+          setOwnerTeamDetails(null);
         }
       } finally {
         setIsLoadingOwner(false);
@@ -675,6 +678,7 @@ function ProjectDetailPanel({
         } else {
           const error = await response.json().catch(() => ({ error: response.statusText }));
           console.error('Failed to fetch users:', error);
+          setUsers([]);
           toast({ variant: "destructive", title: `Failed to load users: ${error.error || 'Unknown error'}` });
         }
       } finally {
@@ -705,6 +709,7 @@ function ProjectDetailPanel({
         } else {
           const error = await response.json().catch(() => ({ error: response.statusText }));
           console.error('Failed to fetch teams:', error);
+          setTeams([]);
           toast({ variant: "destructive", title: `Failed to load teams: ${error.error || 'Unknown error'}` });
         }
       } finally {
@@ -733,6 +738,7 @@ function ProjectDetailPanel({
         } else {
           const error = await response.json().catch(() => ({ error: response.statusText }));
           console.error('Failed to fetch events:', error);
+          setEvents([]);
           toast({ variant: "destructive", title: `Failed to load events: ${error.error || 'Unknown error'}` });
         }
       } finally {

Analysis

Stale data displayed alongside error toasts in internal-support dashboard

What fails: When API calls fail in the internal support dashboard (SearchPanel and ProjectDetailPanel), error toasts are shown but state is not cleared, causing stale data to remain displayed while suggesting an error occurred.

How to reproduce:

  1. Navigate to the internal support dashboard
  2. In SearchPanel: Perform a search that succeeds (gets results)
  3. Trigger a network error or API failure on the next search
  4. Observe: Error toast appears, but previous search results still display instead of "No projects found"

Same issue occurs for:

  • ProjectDetailPanel Users tab: Load users successfully, then trigger API error
  • ProjectDetailPanel Teams tab: Load teams successfully, then trigger API error
  • ProjectDetailPanel Events tab: Load events successfully, then trigger API error
  • ProjectDetailPanel Owner Team section: Load owner team successfully, then trigger API error

Result: Stale data displays with error toast, confusing users who think the old data is current.

Expected: When an API call fails, the associated state (searchResults, users, teams, events, ownerTeamDetails) should be cleared so the UI displays "No [items] found" or empty state, matching the error message.

Root cause: Error handlers in the following locations didn't reset state:

  • SearchPanel (line 472): toast shown but searchResults and total not cleared
  • ProjectDetailPanel - Owner Team (line 650): error logged but ownerTeamDetails not cleared
  • ProjectDetailPanel - Users (line 678): toast shown but users array not cleared
  • ProjectDetailPanel - Teams (line 708): toast shown but teams array not cleared
  • ProjectDetailPanel - Events (line 736): toast shown but events array not cleared

Fix: Added state resets in all error handlers:

  • SearchPanel: setSearchResults([]) and setTotal(0)
  • Owner Team fetch: setOwnerTeamDetails(null)
  • Users fetch: setUsers([])
  • Teams fetch: setTeams([])
  • Events fetch: setEvents([])

This ensures the UI renders the correct empty state messages when errors occur instead of displaying stale data.

@madster456
Copy link
Copy Markdown
Collaborator Author

Closing this as most of this has been surfaced with new UI updates. Almost anything I would look for can now be found on the normal dashboard.

@madster456 madster456 closed this May 5, 2026
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