Skip to content

Add security vulnerabilities table - #199

Merged
Rashmika998 merged 7 commits into
wso2-open-operations:customer-portal-milestone-1from
dileepapeiris:feat/add-security-vulnerabilities-table
Feb 19, 2026
Merged

Rashmika998 merged 7 commits into
wso2-open-operations:customer-portal-milestone-1from
dileepapeiris:feat/add-security-vulnerabilities-table

Conversation

@dileepapeiris

@dileepapeiris dileepapeiris commented Feb 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • Added Security Center with searchable and filterable product vulnerability management.
    • Implemented tabbed interface covering Product Vulnerabilities, Security Advisories, and Component Analysis.
    • Added pagination and severity-based color indicators for vulnerabilities.
    • Reorganized application routing structure for improved project-scoped navigation.

Introduce ProductVulnerabilitiesTable and ProductVulnerabilitiesList components under components/security. ProductVulnerabilitiesTable is a container that handles data fetching (usePostProductVulnerabilitiesSearch, useGetVulnerabilitiesMetaData), debounced search, severity filters, pagination, and maps API responses to a paginated shape. ProductVulnerabilitiesList renders the table UI with loading skeleton, error and empty states, severity/type chips (with color helper), clickable rows, and TablePagination. These components enable searchable, filterable, and paginated display of product vulnerabilities and surface hooks for total-record and error handling.
Introduce two new components for the Product Vulnerabilities UI: ProductVulnerabilitiesTableHeader and ProductVulnerabilitiesTableSkeleton. The header provides title/description, a search field with optional filter icon, and integrates ActiveFilters for managing applied filters. The skeleton component renders configurable placeholder rows for table loading states. Both use @wso2/oxygen-ui primitives and expose props for search, filter callbacks, and rows-per-page.
Introduce a new SecurityPage component for the customer portal webapp. The page displays SecurityStats and a TabBar with three tabs (Product Vulnerabilities, Security Advisories, Component Analysis), renders corresponding components, and manages active tab state. It also tracks vulnerability total records and error state, and navigates to vulnerability detail routes using projectId from route params.
Introduce apps/customer-portal/webapp/src/utils/vulnerabilities.ts with two helper functions: getVulnerabilitySeverityColor and getVulnerabilityStatusColor. Both normalize input and map common severity (Critical/High/Medium/Low) and status (In Progress/Open/Resolved) labels to Oxygen UI color paths, returning a default 'text.secondary' for unknown or missing values.
Introduce a generic React hook useDebouncedValue in apps/customer-portal/webapp/src/hooks. The hook accepts a value and a delay (ms) and returns a debounced value, using a ref-managed timeout that is cleared on updates and cleanup to avoid leaks. Useful for reducing rapid re-renders or throttling API calls. File includes Apache-2.0 license header.
Introduce GenericSubCountCard.tsx: a reusable TypeScript React stat card that displays an icon, value, and label with an optional footer. Uses @wso2/oxygen-ui Box, Card, and Typography, accepts props for label, value, icon, optional color (defaults to "primary.main"), and footerContent, and is exported as the default component.
Import SecurityPage and VulnerabilityDetailsPage and add dedicated security-center subroutes under project routes: index -> SecurityPage and :vulnerabilityId -> VulnerabilityDetailsPage. Minor routing reformatting/nesting cleanup to accommodate the new pages and maintain existing protected routes and fallbacks.
@dileepapeiris dileepapeiris self-assigned this Feb 19, 2026
@coderabbitai

coderabbitai Bot commented Feb 19, 2026 •

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

This PR adds a Security Center feature to the customer portal by restructuring routing with nested project-scoped pages, introducing a SecurityPage with tabbed vulnerability tracking, creating reusable UI components for displaying product vulnerabilities with pagination and filtering, and adding utility functions for vulnerability severity and status color mapping.

Changes

Cohort / File(s) Summary
Routing and Layout
apps/customer-portal/webapp/src/App.tsx
Reorganized routing structure to nest project-scoped routes under /:projectId with AuthGuard and AppLayout, integrating SecurityCenter with vulnerability details route and other project pages.
Reusable UI Component
apps/customer-portal/webapp/src/components/common/GenericSubCountCard.tsx
New compact statistic card component with icon, value, label, optional color, and optional footer content for displaying key metrics.
Security Vulnerabilities Components
apps/customer-portal/webapp/src/components/security/ProductVulnerabilitiesList.tsx, ProductVulnerabilitiesTable.tsx, ProductVulnerabilitiesTableHeader.tsx, ProductVulnerabilitiesTableSkeleton.tsx
New components for rendering and managing a paginated vulnerabilities table with search, filtering by severity, API data fetching with debouncing, and loading/error states.
Security Page
apps/customer-portal/webapp/src/pages/SecurityPage.tsx
New main security page with three tabs (Product Vulnerabilities, Security Advisories, Component Analysis) that manages vulnerability tracking, error handling, and navigation to details.
Utilities and Hooks
apps/customer-portal/webapp/src/utils/vulnerabilities.ts, apps/customer-portal/webapp/src/hooks/useDebouncedValue.ts
New helper functions for mapping vulnerability severity and status to theme colors, and a generic hook for debouncing input values with configurable delay.

Sequence Diagram

sequenceDiagram
    actor User
    participant SecurityPage
    participant ProductVulnerabilitiesTable
    participant API as Backend API
    participant ProductVulnerabilitiesList
    participant Router

    User->>SecurityPage: Navigate to /security-center
    SecurityPage->>ProductVulnerabilitiesTable: Render with callbacks
    ProductVulnerabilitiesTable->>API: POST /products/vulnerabilities/search (debounced)
    API-->>ProductVulnerabilitiesTable: Return vulnerabilities + total records
    ProductVulnerabilitiesTable->>ProductVulnerabilitiesList: Pass fetched data
    ProductVulnerabilitiesList->>User: Display paginated vulnerability table
    User->>ProductVulnerabilitiesList: Click on vulnerability row
    ProductVulnerabilitiesList->>SecurityPage: Trigger onVulnerabilityClick callback
    SecurityPage->>Router: Navigate to /security-center/:vulnerabilityId
    Router->>User: Render VulnerabilityDetailsPage
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

Type/New Feature, Type/Improvement, Type/UX, App/Customer Portal, Area/Frontend, Platform/Web

Suggested reviewers

  • v15a1
  • Rashmika998

Poem

🐰 A security gate now stands so tall,
Vulnerabilities found, none escape at all,
Tables with colors, filters so keen,
Dashboard pristine, a fortress serene,
Hop along safely, protected and free! 🛡️

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author. The required template includes sections for Purpose, Goals, Approach, User stories, Release notes, Documentation, Training, Certification, Marketing, Automation tests, Security checks, Samples, Related PRs, Migrations, Test environment, and Learning—all of which are missing. Provide a comprehensive pull request description following the template, including Purpose, Goals, Approach, test coverage details, documentation links, and security verification confirmations.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add security vulnerabilities table' directly and clearly describes the main feature addition—a security vulnerabilities table component—which aligns with the PR's primary objective of introducing UI and data-fetching for displaying product vulnerabilities.
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@dileepapeiris
dileepapeiris marked this pull request as ready for review February 19, 2026 06:05
@dileepapeiris

Copy link
Copy Markdown
Contributor Author

@coderabbitai Review

@coderabbitai

coderabbitai Bot commented Feb 19, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

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.

Pull request overview

This pull request adds a comprehensive Security Center feature to the customer portal, including a vulnerabilities table, and enhances the Updates section with a pending updates detail page. The changes enable users to view, search, and filter product vulnerabilities, as well as drill down into specific pending update levels for products.

Changes:

  • Added Security Center page with vulnerability management table and tab navigation for future advisories and component analysis features
  • Implemented pending updates detail page showing granular update level information with security/regular classification
  • Updated product update levels model from kebab-case to camelCase field names for TypeScript convention consistency

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
src/utils/vulnerabilities.ts New utility for mapping vulnerability severity and status to UI colors
src/utils/updates.ts Added function to compute pending update levels with security/regular classification
src/hooks/useDebouncedValue.ts New custom hook for debouncing search input values
src/pages/SecurityPage.tsx New security center page with tabbed interface for vulnerabilities, advisories, and components
src/pages/PendingUpdatesPage.tsx New detail page displaying pending updates for a specific product version
src/components/security/* New vulnerability table components with search, filtering, and pagination
src/components/updates/pending-updates/* New components for displaying pending update levels in table format
src/components/common/GenericSubCountCard.tsx Reusable stat card component for displaying counts with icons
src/api/usePostProductVulnerabilitiesSearch.ts New hook for searching vulnerabilities using POST request
src/api/useGetVulnerabilitiesMetaData.ts New hook for fetching vulnerability filter metadata
src/api/useGetProductVulnerability.ts New hook for fetching individual vulnerability details
src/models/responses.ts Updated field names to camelCase; added vulnerability-related interfaces
src/App.tsx Added security-center and pending updates routes
src/components/updates/update-cards/* Updated to support navigation to pending updates page
Comments suppressed due to low confidence (1)

apps/customer-portal/webapp/src/components/updates/update-cards/UpdateProductCard.tsx:117

  • The button should be disabled when onViewPendingUpdates is undefined to prevent users from clicking a non-functional button. Currently, if projectId is not available (see UpdateProductGrid.tsx line 82-90), the button will render but won't respond to clicks, creating a poor user experience. Add the disabled prop: disabled={!onViewPendingUpdates}
        <Button
          fullWidth
          variant="outlined"
          color="warning"
          startIcon={<FileText size={16} />}
          onClick={onViewPendingUpdates}
          sx={{
            textTransform: "none",
          }}
        >
          View {pendingLevels} Pending Updates
        </Button>

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/customer-portal/webapp/src/pages/SecurityPage.tsx
Comment thread apps/customer-portal/webapp/src/App.tsx
Comment thread apps/customer-portal/webapp/src/utils/updates.ts
Comment thread apps/customer-portal/webapp/src/api/useGetProductVulnerability.ts
Comment thread apps/customer-portal/webapp/src/hooks/useDebouncedValue.ts
@dileepapeiris

Copy link
Copy Markdown
Contributor Author

All comments resolved in : #201

Rashmika998
Rashmika998 previously approved these changes Feb 19, 2026
@dileepapeiris
dileepapeiris dismissed Rashmika998’s stale review February 19, 2026 16:30

The merge-base changed after approval.

Rashmika998
Rashmika998 previously approved these changes Feb 19, 2026
@Rashmika998
Rashmika998 merged commit 7dc22b1 into wso2-open-operations:customer-portal-milestone-1 Feb 19, 2026
6 of 7 checks passed

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 17

Caution

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

⚠️ Outside diff range comments (4)
apps/customer-portal/webapp/src/components/dashboard/charts/OutstandingIncidentsChart.tsx (1)

151-152: ⚠️ Potential issue | 🟡 Minor

Stale entityName after title rename.

entityName="outstanding cases" (line 151) does not match the updated title "Outstanding Engagements", producing inconsistent copy in the error state.

🔧 Proposed fix
-  <ErrorIndicator entityName="outstanding cases" />
+  <ErrorIndicator entityName="outstanding engagements" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/dashboard/charts/OutstandingIncidentsChart.tsx`
around lines 151 - 152, The ErrorIndicator usage in
OutstandingIncidentsChart.tsx still passes entityName="outstanding cases" which
is inconsistent with the renamed chart title "Outstanding Engagements"; update
the prop on the ErrorIndicator component (the ErrorIndicator component
invocation in OutstandingIncidentsChart) to use a matching entityName value such
as "outstanding engagements" so the error state copy aligns with the displayed
title.
apps/customer-portal/webapp/src/components/dashboard/charts/ActiveCasesChart.tsx (1)

146-147: ⚠️ Potential issue | 🟡 Minor

Stale entityName after title rename.

entityName="active cases" in the ErrorIndicator (line 146) no longer matches the updated component title "Active Engagements". Error state messaging will be inconsistent with the card heading.

🔧 Proposed fix
-  <ErrorIndicator entityName="active cases" />
+  <ErrorIndicator entityName="active engagements" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/dashboard/charts/ActiveCasesChart.tsx`
around lines 146 - 147, Update the ErrorIndicator's entityName prop to match the
renamed title: replace the stale entityName="active cases" used in the
ActiveCasesChart component with the correct label (e.g., "active engagements" or
"Active Engagements" to match the card heading) so error messages align with the
card heading; ensure the change is applied where ErrorIndicator is rendered and
that casing/spacing matches other UI text for consistency.
apps/customer-portal/webapp/src/components/dashboard/charts/CasesTrendChart.tsx (1)

128-130: ⚠️ Potential issue | 🟡 Minor

Skeleton legend count mismatch with actual data entries.

The skeleton renders 4 placeholders ([1, 2, 3, 4]), but CASES_TREND_CHART_DATA contains 5 severity levels. The ChartLegend will render 5 items when data loads, causing a layout shift.

Proposed fix
-          {[1, 2, 3, 4].map((i) => (
+          {[1, 2, 3, 4, 5].map((i) => (
             <Skeleton key={i} variant="rounded" width={60} height={20} />
           ))}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/dashboard/charts/CasesTrendChart.tsx`
around lines 128 - 130, The skeleton legend currently renders 4 placeholders
([1,2,3,4]) which mismatches CASES_TREND_CHART_DATA's 5 severity items and
causes a layout shift; update the placeholder count to match the real data by
deriving the skeleton items from CASES_TREND_CHART_DATA.length (or use
CASES_TREND_CHART_DATA.map) so the Skeleton loop in CasesTrendChart.tsx produces
the same number of legend placeholders as ChartLegend when data loads.
apps/customer-portal/webapp/src/constants/dashboardConstants.ts (1)

45-73: ⚠️ Potential issue | 🟡 Minor

Tooltip text still references "cases" after labels were renamed to "Engagements".

The label fields were updated (e.g., "Total Engagements", "Active Engagements"), but the tooltipText fields still say "cases" (e.g., "Total number of cases reported…", "Currently active and unresolved cases"). This creates an inconsistent user-facing experience.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/constants/dashboardConstants.ts` around lines
45 - 73, The tooltipText values in DASHBOARD_STATS still refer to "cases" while
the labels use "Engagements"; update the tooltipText for the entries with id
"totalCases", "openCases", "resolvedCases", and "avgResponseTime" in the
DASHBOARD_STATS array to consistently use "engagements" (e.g., "Total number of
engagements reported for this project", "Currently active and unresolved
engagements", "Successfully closed and resolved engagements", "Average time
taken to first respond to an engagement") ensuring text and casing match the
label terminology.
🧹 Nitpick comments (29)
apps/customer-portal/webapp/src/hooks/useDebouncedValue.ts (1)

26-43: useRef is unnecessary — use a local timeout variable instead.

React guarantees the previous effect's cleanup runs before the new effect body executes, so lines 29-31 are always a no-op (the timeout was already cleared by the cleanup on line 40). Storing the ID in a ref adds complexity with no benefit here.

The canonical, idiomatic pattern:

♻️ Simplified implementation
-import { useState, useEffect, useRef } from "react";
+import { useState, useEffect } from "react";

 export function useDebouncedValue<T>(value: T, delayMs: number): T {
   const [debouncedValue, setDebouncedValue] = useState<T>(value);
-  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

   useEffect(() => {
-    if (timeoutRef.current) {
-      clearTimeout(timeoutRef.current);
-    }
-
-    timeoutRef.current = setTimeout(() => {
+    const timeout = setTimeout(() => {
       setDebouncedValue(value);
-      timeoutRef.current = null;
     }, delayMs);

-    return () => {
-      if (timeoutRef.current) {
-        clearTimeout(timeoutRef.current);
-      }
-    };
+    return () => clearTimeout(timeout);
   }, [value, delayMs]);

   return debouncedValue;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/hooks/useDebouncedValue.ts` around lines 26 -
43, The timeoutRef useRef and its checks are unnecessary because React runs the
previous effect's cleanup before the next effect, so remove timeoutRef and the
useRef import, then use a local variable inside the useEffect (e.g., let
timeout: ReturnType<typeof setTimeout> | null) to hold the timer ID, assign it
with setTimeout which calls setDebouncedValue(value) and sets the local to null,
and clearTimeout(timeout) in the return cleanup; keep the same dependency array
([value, delayMs]) and reference the setDebouncedValue call and timeout handling
inside the useEffect function.
apps/customer-portal/webapp/src/components/common/rich-text-editor/__tests__/Editor.test.tsx (2)

30-30: Move the ErrorBannerProvider import to the top-level import block.

This import is placed after the vi.mock(...) block instead of alongside the other imports. ErrorBannerProvider is not mocked and has no ordering dependency on the mock, so there is no reason to place it here. Vitest hoists vi.mock calls before imports anyway, so it is purely a style inconsistency.

♻️ Proposed fix
 import { render, screen, waitFor } from "@testing-library/react";
 import { describe, expect, it, vi } from "vitest";
 import { ThemeProvider, createTheme } from "@wso2/oxygen-ui";
 import Editor from "@components/common/rich-text-editor/Editor";
+import { ErrorBannerProvider } from "@context/error-banner/ErrorBannerContext";

 vi.mock("@hooks/useLogger", () => ({
   useLogger: () => ({
     ...
   }),
 }));
-
-import { ErrorBannerProvider } from "@context/error-banner/ErrorBannerContext";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/common/rich-text-editor/__tests__/Editor.test.tsx`
at line 30, The import for ErrorBannerProvider should be moved out of its
current position after the vi.mock(...) block and placed with the other
top-level imports in Editor.test.tsx; locate the line importing
ErrorBannerProvider from "@context/error-banner/ErrorBannerContext" and relocate
it into the main import block above the vi.mock calls (it is not mocked and has
no ordering dependency), keeping vi.mock(...) where it is since Vitest hoists
mocks.

64-88: Avoid duplicating provider boilerplate — use renderEditor for the initial render.

The new test manually repeats the ThemeProvider + ErrorBannerProvider + Editor tree verbatim, even though renderEditor already encapsulates exactly that wrapper and returns the full RTL result (including rerender). The initial render(...) call on lines 65–71 can be replaced with renderEditor(...). For rerender, a small local helper avoids the repeated JSX:

♻️ Proposed refactor
-  it("updates content when value prop changes from empty", async () => {
-    const { rerender } = render(
-      <ThemeProvider theme={createTheme()}>
-        <ErrorBannerProvider>
-          <Editor value="" onChange={vi.fn()} />
-        </ErrorBannerProvider>
-      </ThemeProvider>,
-    );
-
-    expect(screen.getByTestId("case-description-editor")).toBeInTheDocument();
-
-    rerender(
-      <ThemeProvider theme={createTheme()}>
-        <ErrorBannerProvider>
-          <Editor value="<p>AI Generated Content</p>" onChange={vi.fn()} />
-        </ErrorBannerProvider>
-      </ThemeProvider>,
-    );
+  it("updates content when value prop changes from empty", async () => {
+    const onChange = vi.fn();
+    const { rerender } = renderEditor({ value: "", onChange });
+
+    expect(screen.getByTestId("case-description-editor")).toBeInTheDocument();
+
+    rerender(
+      <ThemeProvider theme={createTheme()}>
+        <ErrorBannerProvider>
+          <Editor value="<p>AI Generated Content</p>" onChange={onChange} />
+        </ErrorBannerProvider>
+      </ThemeProvider>,
+    );

     await waitFor(() => {
       expect(screen.getByTestId("case-description-editor")).toHaveTextContent(
         "AI Generated Content",
       );
     });
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/common/rich-text-editor/__tests__/Editor.test.tsx`
around lines 64 - 88, The test duplicates the
ThemeProvider+ErrorBannerProvider+Editor render tree instead of using the
existing helper; replace the initial render(...) call with renderEditor(...)
(the helper that returns RTL utilities including rerender) and for the
subsequent rerender call use a small local helper (e.g., rerenderWithValue or
call the returned rerender from renderEditor) to re-render the Editor with value
"<p>AI Generated Content</p>" so you avoid repeating ThemeProvider and
ErrorBannerProvider JSX while keeping assertions against
screen.getByTestId("case-description-editor") intact.
apps/customer-portal/webapp/src/components/common/GenericSubCountCard.tsx (1)

22-22: value prop type has redundant members — simplify to ReactNode

ReactNode already includes string, number, null, undefined, boolean, ReactElement, etc. The explicit string | number members are subsumed by ReactNode and add no additional type safety.

♻️ Proposed simplification
-  value: string | number | ReactNode;
+  value: ReactNode;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/components/common/GenericSubCountCard.tsx` at
line 22, The prop type for value in the GenericSubCountCard component is overly
broad because string and number are already included in ReactNode; update the
props/interface that currently declares "value: string | number | ReactNode"
(look for the GenericSubCountCardProps or the component props type) to just
"value: ReactNode", ensure ReactNode is imported from 'react' if not already,
and run the TypeScript typecheck to confirm no call sites need adjustment.
apps/customer-portal/webapp/package.json (1)

10-11: LGTM — new test script is standard and the package versions are valid.

vitest at ^4.0.18 is the current npm latest, and zod at ^4.3.6 is also the current npm latest. All three version constraints are confirmed valid.

One optional follow-up: consider splitting the script into two entries for clarity:

-    "test": "vitest"
+    "test": "vitest run",
+    "test:watch": "vitest"

vitest (no flags) enters watch mode locally and auto-detects CI=true to run once in pipelines, so the single-script form works fine. The split is purely ergonomic — npm test always does a one-shot run regardless of environment.

Also, @testing-library/user-event is absent. If any of the new component tests simulate realistic user input (typing, clicking), consider adding it alongside the existing RTL packages.

+    "@testing-library/user-event": "^14.x.x",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/package.json` around lines 10 - 11, Update
package.json to add ergonomic test scripts and optionally include user-event:
replace the current single "test" script that runs vitest with two scripts
(e.g., "test": "vitest --run" for CI/one-shot and "test:watch": "vitest" for
local watch), keeping "preview" unchanged; also add
"@testing-library/user-event" to devDependencies so component tests can simulate
realistic user interactions alongside the existing testing-library packages.
Ensure you update the "scripts" section entries ("test" and add "test:watch")
and add the "@testing-library/user-event" entry under devDependencies with an
appropriate semver.
apps/customer-portal/webapp/src/components/common/rich-text-editor/Editor.tsx (1)

71-86: Optional: skip the editor.update() call when content is already non-empty

With initialHtml in the dependency array, every parent value prop update (including onChange-driven re-renders on each keystroke) triggers editor.update(). When the editor already has content the update is a no-op for Lexical, but the call itself incurs scheduling overhead on every keystroke. A cheap editor.read()-based pre-check can eliminate those no-op updates:

♻️ Proposed optimisation
   useEffect(() => {
     if (initialHtml) {
+      let isEmpty = true;
+      editor.read(() => {
+        const root = $getRoot();
+        isEmpty = root.getChildrenSize() === 0 ||
+          root.getTextContent().trim() === "";
+      });
+      if (!isEmpty) return;
+
       editor.update(() => {
         const root = $getRoot();
-        const currentContent = root.getTextContent();
-
-        if (currentContent.trim() === "") {
-          const parser = new DOMParser();
-          const dom = parser.parseFromString(initialHtml, "text/html");
-          const nodes = $generateNodesFromDOM(editor, dom);
-          root.clear();
-          root.append(...nodes);
-        }
+        const parser = new DOMParser();
+        const dom = parser.parseFromString(initialHtml, "text/html");
+        const nodes = $generateNodesFromDOM(editor, dom);
+        root.clear();
+        root.append(...nodes);
       });
     }
   }, [editor, initialHtml]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/common/rich-text-editor/Editor.tsx`
around lines 71 - 86, The effect currently calls editor.update on every change
whenever initialHtml is set, causing unnecessary scheduling; change it to first
call editor.read to inspect the current root (via $getRoot().getTextContent())
and only call editor.update to parse/insert nodes (using DOMParser,
$generateNodesFromDOM, root.clear(), root.append(...nodes)) when the content is
empty (trim() === ""); this avoids the noop update overhead by performing a
cheap read pre-check before invoking update.
apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx (1)

167-176: Minor indentation inconsistency inside <TableCell>.

The <Chip> on lines 168–175 is indented with extra leading spaces relative to its parent <TableCell>. Other cells in this file align their content at one level inside the <TableCell>. This is cosmetic only.

🧹 Suggested formatting fix
                  <TableCell>
-                      <Chip
-                        label={row.caseTypes?.label || "--"}
-                        size="small"
-                        variant="outlined"
-                        sx={{
-                          fontWeight: 500,
-                        }}
-                      />
+                    <Chip
+                      label={row.caseTypes?.label || "--"}
+                      size="small"
+                      variant="outlined"
+                      sx={{
+                        fontWeight: 500,
+                      }}
+                    />
                  </TableCell>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/dashboard/cases-table/CasesList.tsx`
around lines 167 - 176, Indentation inside the TableCell for the Chip is
inconsistent with other cells; in CasesList.tsx adjust the spacing so the <Chip
... /> JSX is indented one level inside the TableCell (match the indentation
style used elsewhere in this file) to remove the extra leading spaces around the
Chip element and keep formatting consistent for the TableCell/Chip block.
apps/customer-portal/webapp/src/components/security/ProductVulnerabilitiesTableHeader.tsx (1)

108-152: Consider adding an aria-label to the search field for accessibility.

The TextField relies solely on placeholder text for identification. Screen readers benefit from an explicit aria-label or associated <label> element.

♿ Proposed fix
           <TextField
             sx={{
               width: "100%",
               "& .MuiInputBase-root": {
                 pr: 0.5,
               },
             }}
+            aria-label="Search CVE or component"
             value={searchValue}
             onChange={handleSearchChange}
             placeholder="Search CVE or component"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/security/ProductVulnerabilitiesTableHeader.tsx`
around lines 108 - 152, The search TextField in
ProductVulnerabilitiesTableHeader lacks an explicit accessible name; update the
TextField (component TextField in ProductVulnerabilitiesTableHeader) to include
an aria-label (or add a visible/hidden label prop) that describes the input
(e.g., "Search CVE or component") so screen readers don't rely solely on the
placeholder; ensure the aria-label is added alongside existing props (value,
onChange, placeholder, slotProps) and preserved for the start/end adornments and
handlers like handleSearchChange and onFilterIconClick.
apps/customer-portal/webapp/src/utils/caseCreation.ts (1)

17-18: Consolidate duplicate imports from the same module.

Two separate import statements pull from @models/responses. Merge them into one.

🧹 Proposed fix
-import type { DeploymentProductItem } from "@models/responses";
-import type { CaseClassificationResponse } from "@models/responses";
+import type { DeploymentProductItem, CaseClassificationResponse } from "@models/responses";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/utils/caseCreation.ts` around lines 17 - 18,
Merge the two duplicate import statements from the same module by combining the
type names into a single import from "@models/responses"; specifically replace
the separate imports of DeploymentProductItem and CaseClassificationResponse
with one consolidated import that lists both types together to remove
redundancy.
apps/customer-portal/webapp/src/utils/updates.ts (1)

176-189: Redundant sort and guard condition in getPendingUpdateLevels.

The for loop already iterates levels in ascending order and pushes only those present in updateLevelSet, so pendingLevels is already sorted when sort() is called on line 182 — this call is always a no-op.

Separately, the securityCount > 0 guard on line 189 is redundant: when securityCount === 0, index < 0 is always false regardless, so the condition already reduces to index < securityCount.

♻️ Proposed cleanup
-  pendingLevels.sort((a, b) => a - b);
-
   const securityCount = recommended.availableSecurityUpdatesCount;

   return pendingLevels.map((level, index) => ({
     updateLevel: level,
-    updateType:
-      index < securityCount && securityCount > 0 ? "security" : "regular",
+    updateType: index < securityCount ? "security" : "regular",
   }));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/utils/updates.ts` around lines 176 - 189, The
pendingLevels array in getPendingUpdateLevels is built in ascending order by
iterating level from endingUpdateLevel + 1 to recommendedUpdateLevel and
filtering with updateLevelSet, so the pendingLevels.sort(...) call is redundant
and should be removed; also simplify the updateType assignment by dropping the
unnecessary securityCount > 0 guard and use index < securityCount directly
(securityCount is from recommended.availableSecurityUpdatesCount) when mapping
pendingLevels to objects.
apps/customer-portal/webapp/src/utils/__tests__/updates.test.ts (1)

143-198: Consider adding an edge case where availableSecurityUpdatesCount exceeds pending levels count.

The current tests cover all main paths, but there's no test for when availableSecurityUpdatesCount > pendingLevels.length (e.g., API reports 15 security updates but only 5 levels are pending). In that scenario all pending rows would be labelled "security" — worth asserting explicitly to lock in that behavior.

✅ Suggested additional test
it("labels all rows as security when securityCount exceeds pending count", () => {
  const overRecommended = createUpdateLevelItem({
    productName: "wso2am",
    productBaseVersion: "4.2.0",
    endingUpdateLevel: 11,
    recommendedUpdateLevel: 22,
    availableSecurityUpdatesCount: 20, // more than 11 pending levels
  });
  const rows = getPendingUpdateLevels(overRecommended, productLevels);
  expect(rows.every((r) => r.updateType === "security")).toBe(true);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/utils/__tests__/updates.test.ts` around lines
143 - 198, Add a test case to
apps/customer-portal/webapp/src/utils/__tests__/updates.test.ts that constructs
a RecommendedUpdateLevelItem via createUpdateLevelItem with
availableSecurityUpdatesCount larger than the number of pending levels (e.g.,
set availableSecurityUpdatesCount to 20), call
getPendingUpdateLevels(recommended, productLevels), and assert that every
returned row has updateType === "security" to ensure all pending levels are
labeled security when the security count exceeds pending levels.
apps/customer-portal/webapp/src/components/updates/pending-updates/PendingUpdatesList.tsx (1)

48-49: Double-pass filter for securityCount/regularCount is a micro-inefficiency.

Both counts could be derived in a single pass. This is negligible at current list sizes but worth noting.

♻️ Proposed cleanup
-  const securityCount = pendingRows.filter((r) => r.updateType === "security").length;
-  const regularCount = pendingRows.filter((r) => r.updateType === "regular").length;
+  const securityCount = pendingRows.reduce(
+    (n, r) => n + (r.updateType === "security" ? 1 : 0), 0,
+  );
+  const regularCount = pendingRows.length - securityCount;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/updates/pending-updates/PendingUpdatesList.tsx`
around lines 48 - 49, The code computes securityCount and regularCount by
filtering pendingRows twice; change this to a single pass over pendingRows
(e.g., use Array.prototype.reduce or a single for-loop in PendingUpdatesList) to
accumulate both counts in one traversal, updating the variables securityCount
and regularCount accordingly so the rest of the component uses those computed
values.
apps/customer-portal/webapp/src/components/security/ProductVulnerabilitiesList.tsx (1)

131-143: getVulnerabilitySeverityColor is called twice per row with the same input.

Cache the result in a variable to avoid the double computation on every row render.

♻️ Proposed refactor
-                      <Chip
-                        label={severityLabel}
-                        size="small"
-                        variant="outlined"
-                        sx={{
-                          color: getVulnerabilitySeverityColor(severityLabel),
-                          borderColor: getVulnerabilitySeverityColor(
-                            severityLabel,
-                          ),
-                          fontWeight: 500,
-                        }}
-                      />
+                      {(() => {
+                        const severityColor = getVulnerabilitySeverityColor(severityLabel);
+                        return (
+                          <Chip
+                            label={severityLabel}
+                            size="small"
+                            variant="outlined"
+                            sx={{
+                              color: severityColor,
+                              borderColor: severityColor,
+                              fontWeight: 500,
+                            }}
+                          />
+                        );
+                      })()}

Alternatively, compute severityColor alongside severityLabel at the top of the row's return:

                 const severityLabel = row.severity?.label ?? "--";
+                const severityColor = getVulnerabilitySeverityColor(severityLabel);
                 return (
                   ...
-                          color: getVulnerabilitySeverityColor(severityLabel),
-                          borderColor: getVulnerabilitySeverityColor(severityLabel),
+                          color: severityColor,
+                          borderColor: severityColor,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/security/ProductVulnerabilitiesList.tsx`
around lines 131 - 143, The code calls
getVulnerabilitySeverityColor(severityLabel) twice when rendering the Chip;
compute it once, store it in a local variable (e.g., severityColor) alongside
severityLabel at the top of the row render, and then use that single variable
for both color and borderColor in the Chip sx prop (referencing
getVulnerabilitySeverityColor, severityLabel, severityColor, and the Chip
component).
apps/customer-portal/webapp/src/pages/__tests__/CreateCasePage.test.tsx (2)

130-132: Shared QueryClient causes cross-test cache pollution — create a fresh instance per test.

The queryClient at line 130 is shared across all tests. React Query's in-memory cache persists between tests, so query results resolved in test 1 may be served as cached data in test 2, masking real failures.

♻️ Proposed fix
-const queryClient = new QueryClient({
-  defaultOptions: { queries: { retry: false } },
-});

 describe("CreateCasePage", () => {
+  let queryClient: QueryClient;
+
+  beforeEach(() => {
+    queryClient = new QueryClient({
+      defaultOptions: { queries: { retry: false } },
+    });
+  });
+
+  afterEach(() => {
+    queryClient.clear();
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/pages/__tests__/CreateCasePage.test.tsx`
around lines 130 - 132, The shared QueryClient instance named queryClient (with
defaultOptions: { queries: { retry: false } }) is reused across tests and causes
React Query cache pollution; fix it by creating a fresh QueryClient for each
test (e.g., instantiate a new QueryClient in a beforeEach or inside each test /
render helper) and use that instance when rendering the component or wrapping
with QueryClientProvider so caches don't persist between tests.

150-221: Move vi.mock calls to module level — placing them inside the test body is misleading and fragile.

Vitest hoists vi.mock calls to before imports via a static transform. Placing them inside an it() body after render() (1) makes the code appear sequential when it isn't, (2) applies these mocks globally to all tests in the file implicitly, and (3) will confuse future maintainers. These should be at module level alongside the existing top-level mocks (lines 26–126).

♻️ Proposed structure
-// (remove from inside it() body)
+// At module level, alongside the other vi.mock calls above:
+vi.mock("../../api/useGetProjectDetails", () => ({
+  default: vi.fn(() => ({
+    data: { id: "123", name: "WSO2 Super App", key: "WSA" },
+    isLoading: false,
+    error: null,
+  })),
+}));
+
+vi.mock("../../api/useGetCasesFilters", () => ({ ... }));
+// ... etc.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/pages/__tests__/CreateCasePage.test.tsx`
around lines 150 - 221, The vi.mock calls for hooks like useGetProjectDetails,
useGetCasesFilters, useMockConfig, useGetProjectDeployments,
useGetDeploymentsProducts, usePostCase, and usePostAttachments should be moved
out of the test body to the module scope (alongside the other top-level mocks)
so they run before imports/rendering; locate the vi.mock blocks inside
CreateCasePage.test.tsx and relocate them to the file top-level (before any
imports or before the render call) to ensure they are applied consistently and
not misleadingly scoped to a single it() block.
apps/customer-portal/webapp/src/components/updates/pending-updates/__tests__/PendingUpdatesList.test.tsx (1)

43-87: Use toBeInTheDocument() instead of .toBeDefined() for DOM element assertions.

getByText either returns an element or throws, so expect(...).toBeDefined() is vacuous — it never produces a meaningful failure. Prefer toBeInTheDocument() (via @testing-library/jest-dom) for clarity and intent.

♻️ Example for one assertion (apply consistently)
-    expect(screen.getByText("No pending updates found for this product and version.")).toBeDefined();
+    expect(screen.getByText("No pending updates found for this product and version.")).toBeInTheDocument();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/updates/pending-updates/__tests__/PendingUpdatesList.test.tsx`
around lines 43 - 87, Replace vacuous expect(...).toBeDefined() DOM assertions
in the PendingUpdatesList tests with proper jest-dom assertions: use
expect(screen.getByText(...)).toBeInTheDocument() for single-element checks
(e.g., the empty-state message, summary line, column headers, specific row text
"12","13","14"), and replace
expect(screen.getAllByText(...).length).toBeGreaterThan(0) / .toBe(3) with
expect(screen.getAllByText(...)).toHaveLength(n) or
expect(screen.getAllByText(...).length).toBeGreaterThan(0) converted to
expect(screen.getAllByText(...)).toHaveLength(expect.any(Number) > 0) style
(prefer toHaveLength for exact counts and toHaveLength >0 where needed) so all
assertions use toBeInTheDocument or toHaveLength instead of toBeDefined; ensure
`@testing-library/jest-dom` is available in the test setup.
apps/customer-portal/webapp/src/components/updates/update-cards/UpdateProductGrid.tsx (1)

79-92: Silent click when projectId is absent — consider disabling the button.

When projectId is undefined, onViewPendingUpdates is undefined. UpdateProductCard renders the "View Pending Updates" button unconditionally with onClick={onViewPendingUpdates}, so clicking it silently does nothing. This is currently harmless since UpdatesPage always supplies projectId, but the component could be reused elsewhere.

Consider threading a disabled flag to UpdateProductCard when no handler is available, or omitting the button entirely:

♻️ Suggested guard in UpdateProductCard
- onClick={onViewPendingUpdates}
+ onClick={onViewPendingUpdates}
+ disabled={!onViewPendingUpdates}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/updates/update-cards/UpdateProductGrid.tsx`
around lines 79 - 92, The UpdateProductGrid passes onViewPendingUpdates as
undefined when projectId is absent, causing UpdateProductCard's unconditional
button to be a silent no-op; update the call site to pass a disabled flag (e.g.,
disabled={!projectId}) or only render onViewPendingUpdates when projectId
exists, and update consumers accordingly: in UpdateProductGrid set
onViewPendingUpdates to the navigate callback only if projectId is defined and
also pass a disabled prop (or omit the prop) so UpdateProductCard can render the
button disabled or hide it; reference UpdateProductGrid, UpdateProductCard,
onViewPendingUpdates and projectId when making the change.
apps/customer-portal/webapp/src/App.tsx (1)

37-38: Inconsistent import path style.

These two imports use relative paths ("./pages/...") while every other page import in this file uses the @pages/ alias. Align for consistency.

Proposed fix
-import SecurityPage from "./pages/SecurityPage";
-import VulnerabilityDetailsPage from "./pages/VulnerabilityDetailsPage";
+import SecurityPage from "@pages/SecurityPage";
+import VulnerabilityDetailsPage from "@pages/VulnerabilityDetailsPage";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/App.tsx` around lines 37 - 38, The two
imports for SecurityPage and VulnerabilityDetailsPage use relative paths; update
their import statements so they use the project alias like the other pages
(i.e., import SecurityPage from "@pages/SecurityPage" and
VulnerabilityDetailsPage from "@pages/VulnerabilityDetailsPage") to align with
the rest of App.tsx and ensure consistent module resolution.
apps/customer-portal/webapp/src/api/useGetVulnerabilitiesMetaData.ts (1)

51-55: Error message omits the HTTP status code.

The sibling hook useGetProductVulnerability.ts includes both response.status and response.statusText in its error message, which makes debugging easier. Consider aligning.

Proposed fix
       if (!response.ok) {
         throw new Error(
-          `Error fetching vulnerabilities metadata: ${response.statusText}`,
+          `Error fetching vulnerabilities metadata: ${response.status} ${response.statusText}`,
         );
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/api/useGetVulnerabilitiesMetaData.ts` around
lines 51 - 55, The error thrown in useGetVulnerabilitiesMetaData omits the HTTP
status code; update the error construction inside the response.ok check in
useGetVulnerabilitiesMetaData to include both response.status and
response.statusText (matching the sibling hook useGetProductVulnerability.ts) so
the thrown Error contains the numeric status and the status text for easier
debugging.
apps/customer-portal/webapp/src/components/dashboard/charts/ChartLayout.tsx (1)

52-61: Stale JSDoc — parameter names and types no longer match the interface.

The @param block still references props.outstandingIncidents (should be outstandingCases) and CasesTrendData[] (type no longer exists). Consider updating or removing the per-param docs to stay in sync with ChartLayoutProps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/components/dashboard/charts/ChartLayout.tsx`
around lines 52 - 61, Update the stale JSDoc for the ChartLayout component so it
matches the current ChartLayoutProps: replace references to
props.outstandingIncidents with outstandingCases, remove or update the
nonexistent CasesTrendData type (use the correct type from ChartLayoutProps or
omit the per-param type), and ensure the listed params and types mirror
ChartLayoutProps (or remove the per-param `@param` block entirely and keep a
high-level description). Target the JSDoc immediately above the ChartLayout
component definition to make the names/types consistent.
apps/customer-portal/webapp/src/pages/SecurityPage.tsx (2)

42-58: Static tabs array is recreated on every render.

tabs has no dependency on props or state. Extract it to module scope to avoid unnecessary allocations.

Proposed fix
+const SECURITY_TABS = [
+  {
+    id: "vulnerabilities",
+    label: "Product Vulnerabilities",
+    icon: Siren,
+  },
+  {
+    id: "advisories",
+    label: "Security Advisories",
+    icon: ShieldAlert,
+  },
+  {
+    id: "components",
+    label: "Component Analysis",
+    icon: Package,
+  },
+];
+
 const SecurityPage = (): JSX.Element => {
   const navigate = useNavigate();
   const { projectId } = useParams<{ projectId: string }>();
   const [activeTab, setActiveTab] = useState("vulnerabilities");
   ...
-  const tabs = [
-    {
-      id: "vulnerabilities",
-      label: "Product Vulnerabilities",
-      icon: Siren,
-    },
-    ...
-  ];
   ...
         <TabBar
-          tabs={tabs}
+          tabs={SECURITY_TABS}
           activeTab={activeTab}
           onTabChange={setActiveTab}
         />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/pages/SecurityPage.tsx` around lines 42 - 58,
The tabs array defined as const tabs = [...] inside SecurityPage is recreated on
every render; move this constant to module scope (outside the SecurityPage
component function) so it is allocated once, keeping the same shape and
identifiers (id, label, icon) and names (tabs, and referenced icons Siren,
ShieldAlert, Package) so existing references inside SecurityPage remain
unchanged.

60-87: SecurityStats only reflects vulnerabilities tab data — may be confusing on other tabs.

totalRecords and isError are sourced from ProductVulnerabilitiesTable callbacks. When the user switches to "Advisories" or "Components", the stats header still displays stale vulnerability data (or undefined on first visit). If the stats are vulnerabilities-specific, consider hiding or labeling them accordingly; otherwise, each tab should feed its own stats.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/pages/SecurityPage.tsx` around lines 60 - 87,
SecurityStats is currently fed only by ProductVulnerabilitiesTable
(vulnerabilityTotalRecords, vulnerabilitiesError) so it shows stale/undefined
data when activeTab !== "vulnerabilities"; update the component so stats reflect
the active tab by either (A) hiding SecurityStats unless activeTab ===
"vulnerabilities" or (B) adding per-tab stats state and callbacks: create state
for advisoriesTotalRecords/advisoriesError and
componentsTotalRecords/componentsError and pass their setter callbacks to
SecurityAdvisoriesTable and ComponentAnalysis, then pass the correct pair to
SecurityStats based on activeTab (use activeTab to select which
totalRecords/isError to render); alternatively, when switching tabs via
setActiveTab, reset vulnerabilityTotalRecords/vulnerabilitiesError to avoid
stale values.
apps/customer-portal/webapp/src/pages/__tests__/NoveraChatPage.test.tsx (1)

241-280: Test ends without asserting the classification outcome.

The test verifies the loading spinner appears when "Create Case" is clicked while products are loading, which is good. However, line 279 fires a change event as a re-render trigger but never asserts the final navigation or that classification actually proceeded after products finished loading. This makes the test name ("should wait for products to load before classifying case") only partially verified — it confirms waiting but not the subsequent classification.

Consider adding a waitFor assertion after the trigger to confirm the loading spinner disappears or that navigation occurs once isLoading flips to false.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/pages/__tests__/NoveraChatPage.test.tsx`
around lines 241 - 280, The test currently stops after firing a change event and
never asserts that classification proceeded; after the final
fireEvent.change(input, { target: { value: "Trigger update" } }) call in the
"should wait for products to load before classifying case" test, add a waitFor
(or use findBy*) that asserts the loading spinner
(getByTestId("circular-progress")) disappears or that navigation/classification
happened (e.g., check for expected post-classification text via findByText or
that the route changed after NoveraChatPage completes classification) so the
test verifies the outcome once useAllDeploymentProductsMock transitions to
isLoading: false.
apps/customer-portal/webapp/src/api/useGetProductVulnerability.ts (1)

42-68: Minor inconsistency: try/catch wrapping differs from sibling hook.

This hook wraps the entire queryFn body in try/catch for error logging, while useGetVulnerabilitiesMetaData.ts does not. Both approaches work (react-query surfaces thrown errors either way), but aligning the pattern across sibling hooks would improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/api/useGetProductVulnerability.ts` around
lines 42 - 68, The hook currently wraps the entire queryFn in a try/catch for
logging, which differs from sibling hooks; remove the try/catch in
useGetProductVulnerability's queryFn so errors are allowed to bubble to
react-query (keep existing throws like the baseUrl check and non-ok response),
keep the logger.debug calls for response.status and data but delete the
logger.error and rethrow in the catch; this aligns the pattern with
useGetVulnerabilitiesMetaData while preserving the same thrown errors from
fetchFn/response handling.
apps/customer-portal/webapp/src/pages/CreateCasePage.tsx (2)

130-171: Duplicated inline type for classificationResponse — extract to a shared interface.

The same shape is defined inline at lines 130–143 (for locationState) and again at lines 147–159 (for the state initializer). Consider extracting this to a named interface (or reusing CaseClassificationResponse from the models) to reduce duplication and improve maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/pages/CreateCasePage.tsx` around lines 130 -
171, Extract the duplicated inline type used for locationState and the useState
initializer into a single named interface (e.g., CaseClassificationResponse) or
import the existing CaseClassificationResponse from your models, then replace
the inline type annotations on locationState and the useState generic for
classificationResponse (and keep using setClassificationResponse) to reference
that shared interface; ensure the STORAGE_KEY usage and JSON.parse error
handling remain unchanged.

164-170: Uses console.error instead of the logger available in scope.

Other error paths in this file use showError or structured logging. This catch block uses raw console.error, which bypasses the LoggerProvider.

♻️ Suggested fix

Since useLogger is not currently imported in this file, and adding it may be more than desired, at minimum consider consistency. If the logger is added:

+  const logger = useLogger();
   ...
-      console.error("Failed to parse stored classification data", e);
+      logger.error("Failed to parse stored classification data", e);

Similarly for lines 181–184.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/pages/CreateCasePage.tsx` around lines 164 -
170, The catch block that reads sessionStorage (around the function that
retrieves stored classification using STORAGE_KEY) uses console.error, which
bypasses the app LoggerProvider; replace that console.error usage with the
project logger (useLogger) or the existing showError utility for consistency:
import and call useLogger (or obtain the existing logger used elsewhere in
CreateCasePage) and log a structured error message including the exception and
context ("Failed to parse stored classification data" and STORAGE_KEY), and
mirror the same change for the other similar catch at lines 181–184 so all
storage parse errors use the unified logger/showError instead of console.error.
apps/customer-portal/webapp/src/pages/DashboardPage.tsx (1)

232-247: Fallback objects after || are unreachable — useMemo always returns a value.

activeCases, outstandingCases, and casesTrend are always defined (never null/undefined/0) since useMemo always returns an object or array. The || { ... } / || [] fallbacks are dead code.

♻️ Suggested cleanup
-        outstandingCases={outstandingCases || {
-          low: 0,
-          medium: 0,
-          high: 0,
-          critical: 0,
-          catastrophic: 0,
-          total: 0,
-        }}
-        activeCases={activeCases || {
-          workInProgress: 0,
-          waitingOnClient: 0,
-          waitingOnWso2: 0,
-          total: 0,
-        }}
-        casesTrend={casesTrend || []}
+        outstandingCases={outstandingCases}
+        activeCases={activeCases}
+        casesTrend={casesTrend}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/pages/DashboardPage.tsx` around lines 232 -
247, The fallback objects passed into ChartLayout are dead code because useMemo
always returns defined values; remove the unnecessary `|| { ... }` and `|| []`
fallbacks for the props outstandingCases, activeCases, and casesTrend
(references: ChartLayout props and the variables activeCases, outstandingCases,
casesTrend produced by useMemo) so the component receives the memoized
objects/array directly; if there are concerns about typing, tighten the useMemo
return types instead of keeping the fallback expressions.
apps/customer-portal/webapp/src/components/security/ProductVulnerabilitiesTable.tsx (1)

91-99: onError and onTotalRecordsChange in useEffect deps may cause render loops if callers pass unstable references.

If the parent component passes inline arrow functions for onError or onTotalRecordsChange, these effects re-fire every render. Consider documenting that these callbacks should be stable (wrapped in useCallback) or guard against it internally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/security/ProductVulnerabilitiesTable.tsx`
around lines 91 - 99, The effects currently depend on onError and
onTotalRecordsChange which can cause re-renders if callers pass unstable inline
callbacks; change the implementation to avoid calling those callbacks as deps:
keep refs for the latest onError and onTotalRecordsChange (update the refs in a
separate effect) and then in the existing effects only depend on
data?.totalRecords and isError, using the refs to invoke the callbacks;
alternatively, add internal guards that track the previous totalRecords and
previous isError via useRef and only call the callback when the actual value
changed (useEffect for data.totalRecords -> call latest
onTotalRecordsChangeRef.current when value differs, and likewise for isError ->
onErrorRef.current).
apps/customer-portal/backend/modules/types/types.bal (1)

634-661: Add pagination to CallRequestsResponse to match request contract and established patterns.

The TODO on line 660 flags missing pagination. Request types like CallRequestSearchPayload already include entity:Pagination, but CallRequestsResponse lacks it. Multiple response types in the same file (CaseSearchResponse, CommentsResponse, ProductVulnerabilitySearchResponse) follow the pattern of including int totalRecords and *entity:Pagination. Adding pagination before clients stabilize on the current shape ensures consistency.

Proposed fix
 # Call requests response.
 public type CallRequestsResponse record {|
     # List of call requests
     CallRequest[] callRequests;
-    // TODO: Remove after adding pagination
+    # Total records count
+    int totalRecords;
+    *entity:Pagination;
 |};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/backend/modules/types/types.bal` around lines 634 - 661,
CallRequestsResponse is missing pagination fields; update the
CallRequestsResponse record to include int totalRecords and the optional
entity:Pagination (matching the pattern used by CallRequestSearchPayload and
other responses like CaseSearchResponse, CommentsResponse, and
ProductVulnerabilitySearchResponse). Add a field int totalRecords and a field
*entity:Pagination pagination (or similarly named pagination) to
CallRequestsResponse so clients receive total count and pagination metadata
consistent with the established response shape.

Comment on lines +114 to +120
public isolated function validateDeploymentUpdatePayload(DeploymentUpdatePayload payload) returns string? {
boolean hasDeploymentFields = payload.name !is () && payload.typeKey !is ();

// Check if payload has at least one field
if !hasDeploymentFields && payload.active is () {
return "At least one field (name, typeKey, or active) must be provided for update.";
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find the utils.bal file and examine the relevant code section
fd "utils.bal" --type f | grep entity

Repository: wso2-open-operations/cs-tools

Length of output: 126


🏁 Script executed:

#!/bin/bash
# Examine the validateDeploymentUpdatePayload function and surrounding context
cat -n apps/customer-portal/backend/modules/entity/utils.bal | sed -n '110,140p'

Repository: wso2-open-operations/cs-tools

Length of output: 1317


🏁 Script executed:

#!/bin/bash
# Find the DeploymentUpdatePayload type definition
rg -n "type DeploymentUpdatePayload" --type=bal -A 10

Repository: wso2-open-operations/cs-tools

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Check how validateDeploymentUpdatePayload is called in the codebase
rg -n "validateDeploymentUpdatePayload" --type=bal -B 3 -A 3

Repository: wso2-open-operations/cs-tools

Length of output: 100


🏁 Script executed:

#!/bin/bash
# Search for DeploymentUpdatePayload type definition
rg -n "type DeploymentUpdatePayload" -A 10

Repository: wso2-open-operations/cs-tools

Length of output: 978


🏁 Script executed:

#!/bin/bash
# Search for all usages of validateDeploymentUpdatePayload
rg -n "validateDeploymentUpdatePayload" -B 2 -A 2

Repository: wso2-open-operations/cs-tools

Length of output: 1488


🏁 Script executed:

#!/bin/bash
# Search for DeploymentUpdatePayload in types.bal
find . -name "types.bal" -path "*/entity/*" -exec cat -n {} \;

Repository: wso2-open-operations/cs-tools

Length of output: 29680


Fix logic operator: use || instead of && to allow any deployment field.

On line 115, hasDeploymentFields uses AND, which requires both name and typeKey to be non-nil. However, the error message says "At least one field (name, typeKey, or active) must be provided"—implying OR logic. A user who provides only name (with typeKey nil) will incorrectly receive an error despite providing a valid field.

Fix
-    boolean hasDeploymentFields = payload.name !is () && payload.typeKey !is ();
+    boolean hasDeploymentFields = payload.name !is () || payload.typeKey !is ();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public isolated function validateDeploymentUpdatePayload(DeploymentUpdatePayload payload) returns string? {
boolean hasDeploymentFields = payload.name !is () || payload.typeKey !is ();
// Check if payload has at least one field
if !hasDeploymentFields && payload.active is () {
return "At least one field (name, typeKey, or active) must be provided for update.";
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/backend/modules/entity/utils.bal` around lines 114 -
120, The boolean logic in validateDeploymentUpdatePayload is inverted: change
how hasDeploymentFields is computed so it becomes true when any of payload.name
or payload.typeKey is non-nil (use OR instead of AND), then keep the existing
check that also considers payload.active; update the hasDeploymentFields
expression (referencing variable hasDeploymentFields in function
validateDeploymentUpdatePayload) to use the OR operator so a single provided
field (name or typeKey) is accepted and the error message behavior matches the
intent.

Comment on lines +1232 to +1256
# Get products.
#
# + return - List of products or an error
resource function get products(http:RequestContext ctx, int? offset, int? 'limit) returns entity:ProductsResponse|http:InternalServerError {
authorization:UserInfoPayload|error userInfo = ctx.getWithType(authorization:HEADER_USER_INFO);
if userInfo is error {
return <http:InternalServerError>{
body: {
message: ERR_MSG_USER_INFO_HEADER_NOT_FOUND
}
};
}

entity:ProductsResponse|error response = entity:getProducts(userInfo.idToken, {}); // TODO: Handle pagination
if response is error {
string customError = "Failed to retrieve products.";
log:printError(customError, response);
return <http:InternalServerError>{
body: {
message: customError
}
};
}
return response;
}

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find ProductSearchPayload type definition
ast-grep --pattern 'type ProductSearchPayload record {
  $$$
}'

Repository: wso2-open-operations/cs-tools

Length of output: 55


🏁 Script executed:

# Search for entity:getProducts function to understand its signature and contract
rg -A 10 'function getProducts' --type bal

Repository: wso2-open-operations/cs-tools

Length of output: 100


🏁 Script executed:

# Check the entity module structure to understand the payload types
fd -e bal . integrations/customer-service/modules/entity | head -20

Repository: wso2-open-operations/cs-tools

Length of output: 204


🏁 Script executed:

# Look at how pagination is used in other endpoints in the same file
rg -B 5 -A 10 'resource function get' apps/customer-portal/backend/service.bal | head -100

Repository: wso2-open-operations/cs-tools

Length of output: 3924


🏁 Script executed:

# List Ballerina files in the repository
find . -name "*.bal" -type f | head -20

Repository: wso2-open-operations/cs-tools

Length of output: 1252


🏁 Script executed:

# Search for products endpoint without type restrictions
rg -i "products" --max-count=20 -B 2 -A 8

Repository: wso2-open-operations/cs-tools

Length of output: 50386


🏁 Script executed:

# Search for getProducts function definition
rg "getProducts" --max-count=20 -B 2 -A 15

Repository: wso2-open-operations/cs-tools

Length of output: 2031


🏁 Script executed:

# Find ProductSearchPayload or similar payload types
rg "ProductSearchPayload|ProductsResponse" --max-count=20 -B 2 -A 5

Repository: wso2-open-operations/cs-tools

Length of output: 9728


🏁 Script executed:

# Find Pagination type definition
rg "type Pagination" --max-count=5 -B 1 -A 5 apps/customer-portal/backend/modules/entity/

Repository: wso2-open-operations/cs-tools

Length of output: 618


🏁 Script executed:

# Check entity types.bal more carefully for Pagination
cat apps/customer-portal/backend/modules/entity/types.bal | grep -A 10 "Pagination"

Repository: wso2-open-operations/cs-tools

Length of output: 2937


🏁 Script executed:

# Verify DEFAULT_OFFSET constant
rg "DEFAULT_OFFSET" apps/customer-portal/backend/modules/entity/constants.bal -A 2

Repository: wso2-open-operations/cs-tools

Length of output: 125


Pagination parameters offset and limit are accepted but silently ignored.

The endpoint declares int? offset, int? 'limit as query parameters but passes an empty payload {} to entity:getProducts, causing all requests to use default pagination (offset=0, limit=20) regardless of caller input. This breaks the API contract and can lead to performance issues on large product catalogs.

Either forward the parameters to the payload or remove them from the signature until the TODO is addressed.

Proposed fix
-    resource function get products(http:RequestContext ctx, int? offset, int? 'limit) returns entity:ProductsResponse|http:InternalServerError {
+    resource function get products(http:RequestContext ctx, int? offset, int? 'limit) returns entity:ProductsResponse|http:InternalServerError {
         authorization:UserInfoPayload|error userInfo = ctx.getWithType(authorization:HEADER_USER_INFO);
         if userInfo is error {
             return <http:InternalServerError>{
                 body: {
                     message: ERR_MSG_USER_INFO_HEADER_NOT_FOUND
                 }
             };
         }
 
-        entity:ProductsResponse|error response = entity:getProducts(userInfo.idToken, {}); // TODO: Handle pagination
+        entity:ProductsResponse|error response = entity:getProducts(userInfo.idToken, {
+            pagination: {offset: offset ?: 0, 'limit: 'limit ?: 20}
+        });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Get products.
#
# + return - List of products or an error
resource function get products(http:RequestContext ctx, int? offset, int? 'limit) returns entity:ProductsResponse|http:InternalServerError {
authorization:UserInfoPayload|error userInfo = ctx.getWithType(authorization:HEADER_USER_INFO);
if userInfo is error {
return <http:InternalServerError>{
body: {
message: ERR_MSG_USER_INFO_HEADER_NOT_FOUND
}
};
}
entity:ProductsResponse|error response = entity:getProducts(userInfo.idToken, {
pagination: {offset: offset ?: 0, 'limit: 'limit ?: 20}
});
if response is error {
string customError = "Failed to retrieve products.";
log:printError(customError, response);
return <http:InternalServerError>{
body: {
message: customError
}
};
}
return response;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/backend/service.bal` around lines 1232 - 1256, The
resource function get products currently accepts offset and 'limit but passes an
empty payload to entity:getProducts, so the parameters are ignored; update get
products to construct and pass a pagination payload (e.g., include offset and
limit only when non-nil or use defaults) to entity:getProducts (referencing
entity:getProducts and the resource function get products) so caller-supplied
offset and 'limit are honored, or if you intend to disable pagination remove the
offset and 'limit parameters from the get products signature and related docs
instead; ensure nullable ints are handled before passing to entity:getProducts.

Comment on lines +1782 to +1820
string? validationError = entity:validateCallRequestUpdatePayload(payload);
if validationError is string {
log:printWarn(validationError);
return <http:BadRequest>{
body: {
message: validationError
}
};
}

string|error? validateUtcTimesError = entity:validateUtcTimes(payload.utcTimes);
if validateUtcTimesError is string {
log:printWarn(validateUtcTimesError);
return <http:BadRequest>{
body: {
message: validateUtcTimesError
}
};
}

if validateUtcTimesError is error {
string customError = "Failed to validate UTC times for call request update.";
log:printError(customError, validateUtcTimesError);
return <http:InternalServerError>{
body: {
message: customError
}
};
}

string? validateCallRequestUpdatePayload = entity:validateCallRequestUpdatePayload(payload);
if validateCallRequestUpdatePayload is string {
log:printWarn(validateCallRequestUpdatePayload);
return <http:BadRequest>{
body: {
message: validateCallRequestUpdatePayload
}
};
}

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 | 🟡 Minor

Duplicate call to validateCallRequestUpdatePayload — the validation at lines 1812–1820 is redundant.

entity:validateCallRequestUpdatePayload(payload) is invoked at line 1782 and then again at line 1812 with the exact same payload. The second invocation is dead code that will never trigger a BadRequest (since the first check already returns early on validation failure).

Remove the duplicate block:

Proposed fix
         if validateUtcTimesError is error {
             string customError = "Failed to validate UTC times for call request update.";
             log:printError(customError, validateUtcTimesError);
             return <http:InternalServerError>{
                 body: {
                     message: customError
                 }
             };
         }
 
-        string? validateCallRequestUpdatePayload = entity:validateCallRequestUpdatePayload(payload);
-        if validateCallRequestUpdatePayload is string {
-            log:printWarn(validateCallRequestUpdatePayload);
-            return <http:BadRequest>{
-                body: {
-                    message: validateCallRequestUpdatePayload
-                }
-            };
-        }
-
         entity:CallRequestUpdateResponse|error response = entity:updateCallRequest(userInfo.idToken, callRequestId,
                 payload);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
string? validationError = entity:validateCallRequestUpdatePayload(payload);
if validationError is string {
log:printWarn(validationError);
return <http:BadRequest>{
body: {
message: validationError
}
};
}
string|error? validateUtcTimesError = entity:validateUtcTimes(payload.utcTimes);
if validateUtcTimesError is string {
log:printWarn(validateUtcTimesError);
return <http:BadRequest>{
body: {
message: validateUtcTimesError
}
};
}
if validateUtcTimesError is error {
string customError = "Failed to validate UTC times for call request update.";
log:printError(customError, validateUtcTimesError);
return <http:InternalServerError>{
body: {
message: customError
}
};
}
entity:CallRequestUpdateResponse|error response = entity:updateCallRequest(userInfo.idToken, callRequestId,
payload);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/backend/service.bal` around lines 1782 - 1820, There are
two consecutive calls to entity:validateCallRequestUpdatePayload(payload) —
remove the redundant second block that declares
validateCallRequestUpdatePayload, checks it, logs the warn and returns
BadRequest; keep the original validation at the top (validationError) and ensure
no leftover unused variable names remain (remove the second
validateCallRequestUpdatePayload variable and its if-check/log/return).

<Box sx={{ display: "flex", alignItems: "flex-start", gap: 2 }}>
<Box sx={{ color }}>{icon}</Box>
<Box sx={{ flex: 1 }}>
<Typography variant="h4">{value}</Typography>

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 | 🟡 Minor

Rendering arbitrary ReactNode inside <h4> risks invalid HTML

Typography variant="h4" renders to <h4>. If a caller passes a ReactNode containing block-level elements (div, p, section, etc.) as value, the result is invalid HTML (block element nested inside a heading). Since the type explicitly allows ReactNode, this is easy to trigger accidentally.

Consider restricting value back to string | number if complex nodes are not intended, or changing the container to a Box (renders as div) when value is not a primitive.

🛡️ Safer container for arbitrary ReactNode values
-  <Typography variant="h4">{value}</Typography>
+  {typeof value === "string" || typeof value === "number" ? (
+    <Typography variant="h4">{value}</Typography>
+  ) : (
+    <Box>{value}</Box>
+  )}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Typography variant="h4">{value}</Typography>
{typeof value === "string" || typeof value === "number" ? (
<Typography variant="h4">{value}</Typography>
) : (
<Box>{value}</Box>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/components/common/GenericSubCountCard.tsx` at
line 52, The component GenericSubCountCard currently renders the prop value
directly inside <Typography variant="h4"> which outputs an <h4>, risking invalid
HTML if callers pass a ReactNode containing block elements; update
GenericSubCountCard to restrict the value prop type to string | number (instead
of ReactNode) OR render conditionally: if typeof value is 'string' or 'number'
keep <Typography variant="h4">{value}</Typography>, otherwise wrap the node in a
neutral container (e.g., Box/div) instead of the h4; adjust the value prop type
and any callers accordingly and ensure propTypes/TS types for value and the
rendering logic in GenericSubCountCard reflect this change.

Comment on lines +71 to +86
useEffect(() => {
if (isFirstRender && initialHtml) {
if (initialHtml) {
editor.update(() => {
const root = $getRoot();
// Only set initial value if the editor is empty to avoid overwriting user updates
if (root.getTextContent() === "") {
const currentContent = root.getTextContent();

if (currentContent.trim() === "") {
const parser = new DOMParser();
const dom = parser.parseFromString(initialHtml, "text/html");
const nodes = $generateNodesFromDOM(editor, dom);
root.clear();
root.append(...nodes);
}
});
setIsFirstRender(false);
}
}, [editor, initialHtml, isFirstRender]);
}, [editor, initialHtml]);

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 | 🟡 Minor

getTextContent() guard misses image-only (non-text) editor content

root.getTextContent() only aggregates text nodes. If the user has inserted content composed solely of non-text Lexical nodes (e.g., ImageNodes), the check currentContent.trim() === "" will evaluate to true even though the editor is non-empty, causing initialHtml to be re-applied and overwriting the user's content.

A more reliable guard is to check node count rather than text content:

🛡️ Proposed fix
-        const currentContent = root.getTextContent();
-
-        if (currentContent.trim() === "") {
+        const hasContent = root.getChildrenSize() > 0 &&
+          !(root.getChildrenSize() === 1 &&
+            root.getFirstChild()?.getTextContent?.() === "");
+
+        if (!hasContent) {
           const parser = new DOMParser();
           const dom = parser.parseFromString(initialHtml, "text/html");
           const nodes = $generateNodesFromDOM(editor, dom);
           root.clear();
           root.append(...nodes);
         }

Alternatively, check whether the root has a single empty paragraph (Lexical's default blank-slate state) instead of relying on text content:

-        const currentContent = root.getTextContent();
-
-        if (currentContent.trim() === "") {
+        const children = root.getChildren();
+        const isEffectivelyEmpty =
+          children.length === 0 ||
+          (children.length === 1 &&
+            children[0].getTextContent().trim() === "" &&
+            children[0].getChildrenSize?.() === 0);
+
+        if (isEffectivelyEmpty) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (initialHtml) {
editor.update(() => {
const root = $getRoot();
const hasContent = root.getChildrenSize() > 0 &&
!(root.getChildrenSize() === 1 &&
root.getFirstChild()?.getTextContent?.() === "");
if (!hasContent) {
const parser = new DOMParser();
const dom = parser.parseFromString(initialHtml, "text/html");
const nodes = $generateNodesFromDOM(editor, dom);
root.clear();
root.append(...nodes);
}
});
}
}, [editor, initialHtml]);
Suggested change
useEffect(() => {
if (initialHtml) {
editor.update(() => {
const root = $getRoot();
const children = root.getChildren();
const isEffectivelyEmpty =
children.length === 0 ||
(children.length === 1 &&
children[0].getTextContent().trim() === "" &&
children[0].getChildrenSize?.() === 0);
if (isEffectivelyEmpty) {
const parser = new DOMParser();
const dom = parser.parseFromString(initialHtml, "text/html");
const nodes = $generateNodesFromDOM(editor, dom);
root.clear();
root.append(...nodes);
}
});
}
}, [editor, initialHtml]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/customer-portal/webapp/src/components/common/rich-text-editor/Editor.tsx`
around lines 71 - 86, The current guard uses root.getTextContent() inside
editor.update() which treats image-only content as empty and causes initialHtml
to overwrite non-text nodes; instead, detect an actually-empty editor by
checking the root's node structure (e.g., root.getChildrenSize() === 0 or the
root contains a single empty paragraph node) before applying initialHtml. Update
the condition in the editor.update() block (around $getRoot(),
root.getTextContent(), $generateNodesFromDOM, root.clear(),
root.append(...nodes)) to use a node-count or single-empty-paragraph check
rather than trimming getTextContent().

Comment on lines +96 to +103
if (chatHistory && hasEnvProducts) {
try {
const classificationResponse = await classifyCase({
chatHistory,
envProducts,
region: "EU",
tier: "Tier 1",
});

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 | 🟡 Minor

Hardcoded region and tier will produce incorrect classifications for non-EU / non-Tier-1 customers.

These values should be derived from the project or deployment context. If this is temporary for demo purposes, consider adding a TODO comment.

       if (chatHistory && hasEnvProducts) {
         try {
           const classificationResponse = await classifyCase({
             chatHistory,
             envProducts,
-            region: "EU",
-            tier: "Tier 1",
+            region: "EU",   // TODO: derive from project/deployment context
+            tier: "Tier 1", // TODO: derive from project/deployment context
           });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (chatHistory && hasEnvProducts) {
try {
const classificationResponse = await classifyCase({
chatHistory,
envProducts,
region: "EU", // TODO: derive from project/deployment context
tier: "Tier 1", // TODO: derive from project/deployment context
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx` around lines 96 -
103, The call to classifyCase in NoveraChatPage is passing hardcoded region:
"EU" and tier: "Tier 1", which will misclassify non-EU/non-Tier-1 customers;
update the code to derive region and tier dynamically (e.g., from the
project/deployment context, user/project metadata, props on NoveraChatPage, or
environment/config values) and pass those computed values into classifyCase({
chatHistory, envProducts, region, tier }); if this is only temporary for demos,
add a clear TODO comment above the call indicating the hardcoded values and the
plan to replace them with dynamic resolution.

Comment on lines +123 to +138
const handleCreateCase = useCallback(() => {
setIsCreateCaseLoading(true);

if (isAllProductsLoading) {
setIsWaitingForClassification(true);
} else {
performClassification();
}
}, [isAllProductsLoading, performClassification]);

useEffect(() => {
if (isWaitingForClassification && !isAllProductsLoading) {
setIsWaitingForClassification(false);
performClassification();
}
}, [isWaitingForClassification, isAllProductsLoading, performClassification]);

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 | 🟡 Minor

If isAllProductsLoading never resolves to false, loading state is stuck indefinitely.

When handleCreateCase sets isWaitingForClassification = true (line 127), the cleanup depends on isAllProductsLoading eventually becoming false to trigger the useEffect at line 133. If the products query is disabled, errors without transitioning isLoading, or never fires, isCreateCaseLoading remains true forever with no timeout or fallback.

Consider adding a timeout or checking the error state of the products query to avoid an indefinitely stuck loading indicator.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/pages/NoveraChatPage.tsx` around lines 123 -
138, The loading flow can get stuck if isAllProductsLoading never becomes false;
update handleCreateCase/useEffect to add a fallback that clears
isCreateCaseLoading and triggers performClassification after a timeout or when
the products query errors. Specifically, in the logic around handleCreateCase,
set a timer (e.g., 10s) when setting isWaitingForClassification and store its id
so you can clear it; expand the useEffect watching isWaitingForClassification to
also check a products error flag (or productsQuery.isError) and to clear the
timeout and reset isWaitingForClassification/isCreateCaseLoading before calling
performClassification; ensure any timers are cleaned up in a cleanup function to
avoid leaks and reference the existing symbols handleCreateCase,
isAllProductsLoading, isWaitingForClassification, performClassification, and
isCreateCaseLoading.

Comment on lines 41 to 43

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 | 🟠 Major

No error state handling — a failed API call shows the skeleton indefinitely.

If useGetRecommendedUpdateLevels or useGetProductUpdateLevels errors out, recommendedData remains undefined and isLoading stays true, resulting in a permanent skeleton with no user feedback.

Consider destructuring isError from both hooks and rendering an error message when either fails.

🛡️ Suggested approach
-  const { data: recommendedData } = useGetRecommendedUpdateLevels();
-  const { data: productLevelsData, isLoading: isProductLevelsLoading } =
-    useGetProductUpdateLevels();
+  const { data: recommendedData, isError: isRecommendedError } = useGetRecommendedUpdateLevels();
+  const { data: productLevelsData, isLoading: isProductLevelsLoading, isError: isProductLevelsError } =
+    useGetProductUpdateLevels();

   // ... existing code ...

+  if (isRecommendedError || isProductLevelsError) {
+    return (
+      <Box sx={{ py: 4 }}>
+        <Typography variant="body1" color="error">
+          Failed to load update data. Please try again later.
+        </Typography>
+        <Button startIcon={<ArrowLeft size={16} />} onClick={handleBack} sx={{ mt: 2 }}>
+          Back to Updates
+        </Button>
+      </Box>
+    );
+  }

Also applies to: 95-96

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/pages/PendingUpdatesPage.tsx` around lines 41
- 43, Destructure isError (and optionally error) from both
useGetRecommendedUpdateLevels and useGetProductUpdateLevels hooks (in
PendingUpdatesPage where recommendedData and productLevelsData are fetched) and
update the render logic to show an error state when either isError is true
instead of leaving the skeleton visible; ensure you handle the combined cases
(both loading, one error, both error) so the skeleton only shows while both are
loading and a clear error message/component is rendered when a call fails.

const normalized = severity?.toLowerCase() || "";
if (normalized === "critical") return "error.main";
if (normalized === "high") return "warning.main";
if (normalized === "medium") return "text.disabled";

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 | 🟡 Minor

"medium" severity mapped to "text.disabled" — semantically incorrect color token.

text.disabled is a very-low-opacity grey (rgba(0,0,0,0.26)) intended to signal that "a component or element isn't interactive." Applying it to medium-severity indicators makes a medium-risk vulnerability indistinguishable from a disabled/inactive UI element, which will confuse users. Consider a token that carries actual visual weight, such as "text.secondary" (the current default fallback) or "warning.light".

🎨 Proposed fix
-  if (normalized === "medium") return "text.disabled";
+  if (normalized === "medium") return "warning.light";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (normalized === "medium") return "text.disabled";
if (normalized === "medium") return "warning.light";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/utils/vulnerabilities.ts` at line 26, The
mapping for medium severity incorrectly returns the disabled color token; in the
severity-to-color logic (where the variable normalized is checked, e.g., the
branch that currently does `if (normalized === "medium") return
"text.disabled";`) change the returned token to a semantically appropriate
visible token such as "text.secondary" (or "warning.light") so medium
vulnerabilities are visually distinct from disabled UI elements; update the
branch that checks normalized === "medium" to return the chosen token and ensure
any tests or usages expecting "text.disabled" are adjusted accordingly.

Comment on lines +38 to +40
if (normalized.includes("progress")) return "warning.main";
if (normalized.includes("open")) return "info.main";
if (normalized.includes("resolved")) return "success.main";

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 | 🟡 Minor

includes("open") can produce false-positive matches for statuses like "Reopened".

If the API ever returns a status such as "Reopened" or "Reopen Requested", normalized.includes("open") will match it and return "info.main" instead of falling through to the default. The exact-match pattern used by getVulnerabilitySeverityColor is safer; prefer it here too, or widen the match to anchor the token (e.g. === "open" for exact, or /(^|\s)open(\s|$)/.test()).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/customer-portal/webapp/src/utils/vulnerabilities.ts` around lines 38 -
40, The current checks in getVulnerabilitySeverityColor use
normalized.includes("open") which can false-positive on values like "reopened";
change the check in the function to match tokens exactly (e.g., use normalized
=== "open" or a word-boundary regex like /\bopen\b/.test(normalized)) and update
the other similar includes checks (e.g., "progress", "resolved") if they should
be exact matches so the function returns the correct color for exact statuses.

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.

3 participants