From ea22052959ecbe0f3a041a14e8d09c2b546bd2f6 Mon Sep 17 00:00:00 2001 From: Sajith Ekanayaka Date: Wed, 24 Jun 2026 07:21:25 +0000 Subject: [PATCH] [CSM Portal] remove runtime mock-data mode and its dead filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the header mock-vs-live toggle and the entire mock data layer: the MockModeToggle + MockModeProvider, isMockMode() and its branches in every data hook, the seeded cases/comments/dashboard mock modules, and the mock-only client-side cases filter. The app now always calls the live backend. With mock data gone, drop the cases-filter controls that only ever worked against it and have no live /cases/search support: the My-ABT/ All-customers scope toggle, and the SLA and product filters (plus the orphaned, never-rendered MyQueueSection / SlaAtRiskSection dashboard widgets built around them). The assignee filter is KEPT (disabled with a 'coming soon' note) — it will be wired to the live backend in a follow-up. The cases filter keeps search, severity, state, and project, all backed by the search API. --- apps/csm-portal/webapp/src/AppWithConfig.tsx | 5 +- .../webapp/src/api/backend/client.ts | 8 - .../webapp/src/api/useDirectoryUsers.ts | 28 +- .../webapp/src/components/header/Actions.tsx | 2 - .../src/components/header/MockModeToggle.tsx | 76 - .../webapp/src/config/authConfig.ts | 2 - .../src/context/mock-mode/MockModeContext.tsx | 125 -- .../csm-cases/api/mocks/casesMocks.ts | 1228 ----------------- .../csm-cases/api/mocks/commentsMocks.ts | 787 ----------- .../csm-cases/api/useCsmCaseAttachments.ts | 48 +- .../csm-cases/api/useCsmCaseComments.ts | 33 +- .../csm-cases/api/useFindMyOngoingCases.ts | 18 +- .../csm-cases/api/useGetCsmCaseDetail.ts | 16 +- .../features/csm-cases/api/useGetCsmCases.ts | 39 +- .../features/csm-cases/api/usePatchCsmCase.ts | 26 +- .../features/csm-cases/api/usePostCsmCase.ts | 18 +- .../csm-cases/api/useQuickCaseSearch.ts | 31 +- .../components/CaseActionBar.test.tsx | 56 +- .../csm-cases/components/CasesFilterBar.tsx | 142 +- .../csm-cases/pages/CsmCaseDetailPage.tsx | 2 +- .../features/csm-cases/pages/CsmCasesPage.tsx | 15 +- .../src/features/csm-cases/types/csmCases.ts | 2 - .../csm-cases/utils/casesClientFilter.ts | 74 - .../csm-cases/utils/casesFiltersUrl.test.ts | 16 +- .../csm-cases/utils/casesFiltersUrl.ts | 27 +- .../csm-dashboard/api/mocks/dashboardMocks.ts | 336 ----- .../csm-dashboard/api/useCaseComposition.ts | 121 +- .../csm-dashboard/api/useCaseCountsMatrix.ts | 16 +- .../csm-dashboard/api/useGetCsmDashboard.ts | 17 +- .../components/MyQueueSection.tsx | 215 --- .../components/SlaAtRiskSection.tsx | 157 --- apps/csm-portal/webapp/src/main.tsx | 5 - apps/csm-portal/webapp/vite.config.ts | 6 +- 33 files changed, 144 insertions(+), 3553 deletions(-) delete mode 100644 apps/csm-portal/webapp/src/components/header/MockModeToggle.tsx delete mode 100644 apps/csm-portal/webapp/src/context/mock-mode/MockModeContext.tsx delete mode 100644 apps/csm-portal/webapp/src/features/csm-cases/api/mocks/casesMocks.ts delete mode 100644 apps/csm-portal/webapp/src/features/csm-cases/api/mocks/commentsMocks.ts delete mode 100644 apps/csm-portal/webapp/src/features/csm-cases/utils/casesClientFilter.ts delete mode 100644 apps/csm-portal/webapp/src/features/csm-dashboard/api/mocks/dashboardMocks.ts delete mode 100644 apps/csm-portal/webapp/src/features/csm-dashboard/components/MyQueueSection.tsx delete mode 100644 apps/csm-portal/webapp/src/features/csm-dashboard/components/SlaAtRiskSection.tsx diff --git a/apps/csm-portal/webapp/src/AppWithConfig.tsx b/apps/csm-portal/webapp/src/AppWithConfig.tsx index 9b58830278..6c0a7f9623 100644 --- a/apps/csm-portal/webapp/src/AppWithConfig.tsx +++ b/apps/csm-portal/webapp/src/AppWithConfig.tsx @@ -22,7 +22,6 @@ import AppErrorBoundary from "@components/error/AppErrorBoundary"; import { AsgardeoProvider } from "@asgardeo/react"; import { loggerConfig } from "@config/loggerConfig"; import LoggerProvider from "@context/logger/LoggerProvider"; -import { MockModeProvider } from "@context/mock-mode/MockModeContext"; import { ThemePreferenceProvider } from "@context/theme/ThemePreferenceContext"; import { authConfig } from "@config/authConfig"; @@ -90,9 +89,7 @@ export default function AppWithConfig(): JSX.Element { - - - + {ReactQueryDevtools && ( diff --git a/apps/csm-portal/webapp/src/api/backend/client.ts b/apps/csm-portal/webapp/src/api/backend/client.ts index 3730a0ea62..cc4817e88a 100644 --- a/apps/csm-portal/webapp/src/api/backend/client.ts +++ b/apps/csm-portal/webapp/src/api/backend/client.ts @@ -19,14 +19,6 @@ import { useAuthApiClient } from "@hooks/useAuthApiClient"; import type { BeErrorPayload } from "@api/backend/types"; import { CORRELATION_ID_HEADER } from "@utils/correlationId"; -/** - * `true` when the mock toggle is on. Hooks should branch on this and call - * the backend client only in LIVE mode. - */ -export function isMockMode(): boolean { - return !!window.config?.CSM_PORTAL_USE_MOCKS; -} - export function backendBaseUrl(): string { const baseUrl = window.config?.CSM_PORTAL_BACKEND_BASE_URL; if (!baseUrl) { diff --git a/apps/csm-portal/webapp/src/api/useDirectoryUsers.ts b/apps/csm-portal/webapp/src/api/useDirectoryUsers.ts index 3479ffd181..dc49c5911e 100644 --- a/apps/csm-portal/webapp/src/api/useDirectoryUsers.ts +++ b/apps/csm-portal/webapp/src/api/useDirectoryUsers.ts @@ -15,9 +15,8 @@ // under the License. import { useQuery, type UseQueryResult } from "@tanstack/react-query"; -import { useLogger } from "@hooks/useLogger"; import { ApiQueryKeys } from "@constants/apiConstants"; -import { isMockMode, useBackendApi } from "@api/backend/client"; +import { useBackendApi } from "@api/backend/client"; /** * Minimal user-directory entry: just what UI affordances like the cases @@ -29,19 +28,6 @@ export interface DirectoryUser { email: string; } -const MOCK_LATENCY_MS = 150; - -// A small mock directory so the assignee picker is populated in mock mode -// without coupling to the csm-admin mock data set. -const MOCK_DIRECTORY: DirectoryUser[] = [ - { name: "Sajith Ekanayaka", email: "sajithe@wso2.com" }, - { name: "Renee Park", email: "renee.park@example.com" }, - { name: "Marcus Webb", email: "marcus.webb@example.com" }, - { name: "Lena Fischer", email: "lena.fischer@example.com" }, - { name: "Arjun Nair", email: "arjun.nair@example.com" }, - { name: "Priya Sharma", email: "priya.sharma@example.com" }, -]; - /** Raw user shape the backend may return; mapped defensively to DirectoryUser. */ interface RawDirectoryUser { name?: string; @@ -58,22 +44,16 @@ function toDirectoryUser(u: RawDirectoryUser): DirectoryUser { } /** - * Shared user-directory lookup. Backed by `POST /users/search` in live mode - * (the canonical backend route — there is no `GET /users`), and a small mock - * set under the mock toggle. Returns a lightweight {@link DirectoryUser} list. + * Shared user-directory lookup. Backed by `POST /users/search` + * (the canonical backend route — there is no `GET /users`). Returns a + * lightweight {@link DirectoryUser} list. */ export function useDirectoryUsers(): UseQueryResult { - const logger = useLogger(); const api = useBackendApi(); return useQuery({ queryKey: [ApiQueryKeys.CSM_ADMIN_USERS, "directory"], queryFn: async (): Promise => { - if (isMockMode()) { - logger.debug("[useDirectoryUsers] returning mock directory"); - await new Promise((r) => setTimeout(r, MOCK_LATENCY_MS)); - return MOCK_DIRECTORY; - } const res = await api.post, unknown>( "/users/search", {}, diff --git a/apps/csm-portal/webapp/src/components/header/Actions.tsx b/apps/csm-portal/webapp/src/components/header/Actions.tsx index f218e860ea..2bfde6b316 100644 --- a/apps/csm-portal/webapp/src/components/header/Actions.tsx +++ b/apps/csm-portal/webapp/src/components/header/Actions.tsx @@ -23,7 +23,6 @@ import { import type { JSX } from "react"; import { useAsgardeo } from "@asgardeo/react"; import UserProfile from "@components/header/UserProfile"; -import MockModeToggle from "@components/header/MockModeToggle"; import ThemeSelect from "@components/header/ThemeSelect"; import RecentViewsButton from "@features/csm-recent/components/RecentViewsButton"; import PinThisPageButton from "@features/csm-recent/components/PinThisPageButton"; @@ -33,7 +32,6 @@ export default function Actions(): JSX.Element { return ( - {isSignedIn && } diff --git a/apps/csm-portal/webapp/src/components/header/MockModeToggle.tsx b/apps/csm-portal/webapp/src/components/header/MockModeToggle.tsx deleted file mode 100644 index afdf1bf661..0000000000 --- a/apps/csm-portal/webapp/src/components/header/MockModeToggle.tsx +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -import { Box, Switch, Tooltip, Typography } from "@wso2/oxygen-ui"; -import type { JSX } from "react"; -import { useMockMode } from "@context/mock-mode/MockModeContext"; - -/** - * Header switch to flip between seeded mock data and the live backend at - * runtime. Rendered only when the deployment opts in via - * `window.config.CSM_PORTAL_ALLOW_MOCK_TOGGLE`, so it never appears in a - * production build where an engineer could unknowingly act on fake data. - * - * The flag flip is handled by `MockModeProvider`, which persists the choice to - * localStorage and invalidates every cached query so the next render refetches - * through the newly-selected code path. - */ -export default function MockModeToggle(): JSX.Element | null { - // Hook must run unconditionally; the visibility gate comes after it. - const { useMocks, toggle } = useMockMode(); - - if (!window.config?.CSM_PORTAL_ALLOW_MOCK_TOGGLE) { - return null; - } - - return ( - - - - Mock - - - - - ); -} diff --git a/apps/csm-portal/webapp/src/config/authConfig.ts b/apps/csm-portal/webapp/src/config/authConfig.ts index 9b2516e482..8428cf7f23 100644 --- a/apps/csm-portal/webapp/src/config/authConfig.ts +++ b/apps/csm-portal/webapp/src/config/authConfig.ts @@ -38,8 +38,6 @@ declare global { CSM_PORTAL_ANNOUNCEMENT_BANNER_VISIBLE?: boolean; CSM_PORTAL_ANNOUNCEMENT_BANNER_STORAGE_KEY?: string; CSM_PORTAL_ANNOUNCEMENT_BANNER_HTML?: string; - CSM_PORTAL_USE_MOCKS?: boolean; - CSM_PORTAL_ALLOW_MOCK_TOGGLE?: boolean; }; } } diff --git a/apps/csm-portal/webapp/src/context/mock-mode/MockModeContext.tsx b/apps/csm-portal/webapp/src/context/mock-mode/MockModeContext.tsx deleted file mode 100644 index e718ca62de..0000000000 --- a/apps/csm-portal/webapp/src/context/mock-mode/MockModeContext.tsx +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -/* eslint-disable react-refresh/only-export-components -- Provider component and its useXxx hook are colocated per the repo's context idiom (fast-refresh DX only) */ - -import { useQueryClient } from "@tanstack/react-query"; -import { - createContext, - useCallback, - useContext, - useMemo, - useState, - type JSX, - type ReactNode, -} from "react"; - -const STORAGE_KEY = "csm.useMocks"; - -interface MockModeContextValue { - /** When true, hooks short-circuit and return seeded mock data. */ - useMocks: boolean; - /** Set the flag directly. */ - setUseMocks: (next: boolean) => void; - /** Convenience: flip the flag. */ - toggle: () => void; -} - -const MockModeContext = createContext(null); - -/** - * Read the saved mock preference at module load and apply it to - * `window.config.CSM_PORTAL_USE_MOCKS` BEFORE any hook reads the flag. - * - * This must run before React mounts, so callers should invoke it from - * `main.tsx` next to the other early-boot side effects (e.g. Prism, CSS). - */ -export function hydrateMockModeFromStorage(): void { - if (typeof window === "undefined") return; - try { - const saved = window.localStorage.getItem(STORAGE_KEY); - if (saved !== null) { - // Mutate `window.config` in place so existing hooks (which read - // `window.config?.CSM_PORTAL_USE_MOCKS` directly) pick up the - // override without any code change. - const cfg = (window as unknown as { config?: { CSM_PORTAL_USE_MOCKS?: boolean } }).config; - if (cfg) { - cfg.CSM_PORTAL_USE_MOCKS = saved === "1"; - } - } - } catch { - // localStorage can throw in sandboxed contexts — ignore and use the - // build-time default already on window.config. - } -} - -function readInitial(): boolean { - if (typeof window === "undefined") return false; - try { - const saved = window.localStorage.getItem(STORAGE_KEY); - if (saved !== null) return saved === "1"; - } catch { - /* fall through to config default */ - } - const cfg = (window as unknown as { config?: { CSM_PORTAL_USE_MOCKS?: boolean } }).config; - return !!cfg?.CSM_PORTAL_USE_MOCKS; -} - -/** - * Provides the runtime mock-vs-backend toggle. Lives inside the - * QueryClientProvider so a toggle can invalidate every cached query and - * force the next render to refetch through whichever code path the new - * flag selects. - */ -export function MockModeProvider({ children }: { children: ReactNode }): JSX.Element { - const qc = useQueryClient(); - const [useMocks, setUseMocksState] = useState(() => readInitial()); - - const setUseMocks = useCallback( - (next: boolean): void => { - setUseMocksState(next); - try { - window.localStorage.setItem(STORAGE_KEY, next ? "1" : "0"); - } catch { - /* ignore */ - } - const cfg = (window as unknown as { config?: { CSM_PORTAL_USE_MOCKS?: boolean } }).config; - if (cfg) cfg.CSM_PORTAL_USE_MOCKS = next; - // Drop all cached results so the next render takes the new code path. - qc.invalidateQueries(); - }, - [qc], - ); - - const value = useMemo( - () => ({ - useMocks, - setUseMocks, - toggle: () => setUseMocks(!useMocks), - }), - [useMocks, setUseMocks], - ); - - return {children}; -} - -export function useMockMode(): MockModeContextValue { - const ctx = useContext(MockModeContext); - if (!ctx) { - throw new Error("useMockMode must be used within a MockModeProvider"); - } - return ctx; -} diff --git a/apps/csm-portal/webapp/src/features/csm-cases/api/mocks/casesMocks.ts b/apps/csm-portal/webapp/src/features/csm-cases/api/mocks/casesMocks.ts deleted file mode 100644 index 8e0d410ff9..0000000000 --- a/apps/csm-portal/webapp/src/features/csm-cases/api/mocks/casesMocks.ts +++ /dev/null @@ -1,1228 +0,0 @@ -// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -import type { - CaseState, - DashboardScope, -} from "@features/csm-dashboard/types/abtDashboard"; -import type { - CaseAttachment, - CaseAuditEntry, - CaseCustomerContext, - CaseLinkedItem, - CaseProductContext, - CaseSlaClock, - CaseTag, - CaseTimeLogEntry, - CaseWatcher, - CsmCaseDetail, - CsmCaseRow, -} from "@features/csm-cases/types/csmCases"; - -const minutesAgo = (n: number): string => - new Date(Date.now() - n * 60_000).toISOString(); - -/** - * Map a case (by subject + project name) to its affected WSO2 product. The - * full product context (version, update level, environment) is built off this - * in `deriveProduct` below; the list row only carries the product name. - */ -function deriveProductName(subject: string, projectName: string): string { - const s = subject.toLowerCase(); - const p = projectName.toLowerCase(); - if (s.includes("identity server") || p.includes("iam")) - return "WSO2 Identity Server"; - if (p.includes("asgardeo") || s.includes("asgardeo")) return "WSO2 Asgardeo"; - if (p.includes("api manager") || s.includes("api manager")) - return "WSO2 API Manager"; - if (p.includes("choreo") || s.includes("choreo")) return "WSO2 Choreo"; - if (p.includes("integrator") || p.includes("mi") || p.includes("streaming")) - return "WSO2 Micro Integrator"; - if (p.includes("open banking") || s.includes("openbanking")) - return "WSO2 Open Banking"; - return "WSO2 Platform"; -} - -type CaseSeed = Omit; - -const ABT_CASE_SEEDS: CaseSeed[] = [ - { - id: "case-1001", - caseNumber: "CS-1001", - subject: "Identity Server token issuance latency spike", - customer: "Acme Financial", - accountId: "acc-001", - projectId: "prj-acme-iam-prod", - projectName: "IAM Production", - severity: "S1", - state: "work_in_progress", - assignee: "Sajith Ekanayaka", - assigneeIsMe: true, - slaClockType: "first_response", - minutesToBreach: -22, - createdAt: minutesAgo(60 * 8), - updatedAt: minutesAgo(22), - }, - { - id: "case-1002", - caseNumber: "CS-1002", - subject: "API Manager gateway 502 during peak", - customer: "Initech Systems", - accountId: "acc-003", - projectId: "prj-initech-apim", - projectName: "API Manager", - severity: "S0", - state: "solution_proposed", - assignee: "Sajith Ekanayaka", - assigneeIsMe: true, - slaClockType: "resolution", - minutesToBreach: 12, - createdAt: minutesAgo(60 * 3), - updatedAt: minutesAgo(4), - }, - { - id: "case-1003", - caseNumber: "CS-1003", - subject: "MI cluster failed to start after upgrade", - customer: "Initech Systems", - accountId: "acc-003", - projectId: "prj-initech-mi", - projectName: "Micro Integrator", - severity: "S1", - state: "solution_proposed", - assignee: "Sajith Ekanayaka", - assigneeIsMe: true, - slaClockType: "resolution", - minutesToBreach: 600, - createdAt: minutesAgo(60 * 6), - updatedAt: minutesAgo(60 + 30), - }, - { - id: "case-1004", - caseNumber: "CS-1004", - subject: "OIDC userinfo claims missing groups", - customer: "Acme Financial", - accountId: "acc-001", - projectId: "prj-acme-iam-prod", - projectName: "IAM Production", - severity: "S2", - state: "work_in_progress", - assignee: "Sajith Ekanayaka", - assigneeIsMe: true, - slaClockType: "first_response", - minutesToBreach: 180, - createdAt: minutesAgo(75), - updatedAt: minutesAgo(60), - }, - { - id: "case-1005", - caseNumber: "CS-1005", - subject: "Choreo deployment stuck in 'Provisioning'", - customer: "Umbrella Health", - accountId: "acc-005", - projectId: "prj-umbrella-choreo", - projectName: "Choreo Platform", - severity: "S2", - state: "closed", - assignee: "Sajith Ekanayaka", - assigneeIsMe: true, - slaClockType: "resolution", - minutesToBreach: 0, - createdAt: minutesAgo(60 * 5), - updatedAt: minutesAgo(60), - }, - { - id: "case-1006", - caseNumber: "CS-1006", - subject: "SAML signature validation fails after SF cert rotation", - customer: "Globex Corp", - accountId: "acc-002", - projectId: "prj-globex-iam", - projectName: "IAM", - severity: "S2", - state: "closed", - assignee: "Sajith Ekanayaka", - assigneeIsMe: true, - slaClockType: "resolution", - minutesToBreach: 0, - createdAt: minutesAgo(60 * 12), - updatedAt: minutesAgo(60 * 4 + 30), - }, - { - id: "case-1007", - caseNumber: "CS-1007", - subject: "Streaming Integrator backpressure", - customer: "Initech Systems", - accountId: "acc-003", - projectId: "prj-initech-si", - projectName: "Streaming Integrator", - severity: "S1", - state: "awaiting_info", - assignee: "Priya N.", - assigneeIsMe: false, - slaClockType: "resolution", - minutesToBreach: -180, - createdAt: minutesAgo(60 * 30), - updatedAt: minutesAgo(60 * 4), - }, - { - id: "case-1008", - caseNumber: "CS-1008", - subject: "Asgardeo SCIM bulk import failing", - customer: "Umbrella Health", - accountId: "acc-005", - projectId: "prj-umbrella-asgardeo", - projectName: "Asgardeo Tenant", - severity: "S2", - state: "work_in_progress", - assignee: "Dilan W.", - assigneeIsMe: false, - slaClockType: "first_response", - minutesToBreach: 25, - createdAt: minutesAgo(60 * 4), - updatedAt: minutesAgo(35), - }, - { - id: "case-1009", - caseNumber: "CS-1009", - subject: "Customer asking about FAPI conformance", - customer: "Acme Financial", - accountId: "acc-001", - projectId: "prj-acme-openbanking", - projectName: "Open Banking", - severity: "S3", - state: "open", - assignee: "Unassigned", - assigneeIsMe: false, - slaClockType: "ack", - minutesToBreach: 720, - createdAt: minutesAgo(60 * 2), - updatedAt: minutesAgo(60 * 2), - }, - { - id: "case-1010", - caseNumber: "CS-1010", - subject: "Choreo build cache missing after region failover", - customer: "Globex Corp", - accountId: "acc-002", - projectId: "prj-globex-choreo", - projectName: "Choreo Build", - severity: "S2", - state: "solution_proposed", - assignee: "Sajith Ekanayaka", - assigneeIsMe: true, - slaClockType: "resolution", - minutesToBreach: 480, - createdAt: minutesAgo(60 * 18), - updatedAt: minutesAgo(60 * 2), - }, - { - id: "case-1011", - caseNumber: "CS-1011", - subject: "Identity Server LDAP user search slow", - customer: "Globex Corp", - accountId: "acc-002", - projectId: "prj-globex-iam", - projectName: "IAM", - severity: "S3", - state: "solution_proposed", - assignee: "Sajith Ekanayaka", - assigneeIsMe: true, - slaClockType: "resolution", - minutesToBreach: 1200, - createdAt: minutesAgo(60 * 24), - updatedAt: minutesAgo(60 * 6), - }, - { - id: "case-1012", - caseNumber: "CS-1012", - subject: "API Manager publisher UI 403 for tenant admin", - customer: "Soylent Industries", - accountId: "acc-004", - projectId: "prj-soylent-apim", - projectName: "API Manager", - severity: "S3", - state: "work_in_progress", - assignee: "Tharindu A.", - assigneeIsMe: false, - slaClockType: "first_response", - minutesToBreach: 360, - createdAt: minutesAgo(60 * 3), - updatedAt: minutesAgo(60), - }, - { - id: "case-1013", - caseNumber: "CS-1013", - subject: "Update level for IS 7.1.0 — review request", - customer: "Acme Financial", - accountId: "acc-001", - projectId: "prj-acme-iam-prod", - projectName: "IAM Production", - severity: "S3", - state: "open", - assignee: "Unassigned", - assigneeIsMe: false, - slaClockType: "ack", - minutesToBreach: 1080, - createdAt: minutesAgo(60), - updatedAt: minutesAgo(60), - }, - { - id: "case-1014", - caseNumber: "CS-1014", - subject: "Webhook signature verification failing in QA", - customer: "Umbrella Health", - accountId: "acc-005", - projectId: "prj-umbrella-choreo", - projectName: "Choreo Platform", - severity: "S3", - state: "work_in_progress", - assignee: "Priya N.", - assigneeIsMe: false, - slaClockType: "resolution", - minutesToBreach: 900, - createdAt: minutesAgo(60 * 36), - updatedAt: minutesAgo(60 * 8), - }, - { - id: "case-1015", - caseNumber: "CS-1015", - subject: "Choreo runtime OOM under sustained load", - customer: "Initech Systems", - accountId: "acc-003", - projectId: "prj-initech-choreo", - projectName: "Choreo Runtime", - severity: "S2", - state: "work_in_progress", - assignee: "Sajith Ekanayaka", - assigneeIsMe: true, - slaClockType: "first_response", - minutesToBreach: 60, - createdAt: minutesAgo(60 * 2), - updatedAt: minutesAgo(15), - }, - { - id: "case-1016", - caseNumber: "CS-1016", - subject: "How do I rotate signing keys without downtime?", - customer: "Soylent Industries", - accountId: "acc-004", - projectId: "prj-soylent-iam", - projectName: "IAM", - severity: "S4", - state: "open", - assignee: "Unassigned", - assigneeIsMe: false, - slaClockType: "ack", - minutesToBreach: 2160, - createdAt: minutesAgo(60 * 6), - updatedAt: minutesAgo(60 * 6), - }, - { - id: "case-1017", - caseNumber: "CS-1017", - subject: "Closing: migration completed successfully", - customer: "Globex Corp", - accountId: "acc-002", - projectId: "prj-globex-iam", - projectName: "IAM", - severity: "S3", - state: "closed", - assignee: "Maya R.", - assigneeIsMe: false, - slaClockType: "resolution", - minutesToBreach: 0, - createdAt: minutesAgo(60 * 80), - updatedAt: minutesAgo(60 * 10), - }, - { - id: "case-1018", - caseNumber: "CS-1018", - subject: "Tenant admin lost MFA device", - customer: "Acme Financial", - accountId: "acc-001", - projectId: "prj-acme-iam-prod", - projectName: "IAM Production", - severity: "S2", - state: "waiting_on_wso2", - assignee: "Sajith Ekanayaka", - assigneeIsMe: true, - slaClockType: "ack", - minutesToBreach: 30, - createdAt: minutesAgo(60 * 48), - updatedAt: minutesAgo(10), - }, -]; - -const ALL_EXTRA_CASE_SEEDS: CaseSeed[] = [ - { - id: "case-2001", - caseNumber: "CS-2001", - subject: "Open Banking sandbox cert mismatch", - customer: "Wayne Enterprises", - accountId: "acc-101", - projectId: "prj-wayne-openbanking", - projectName: "Open Banking Sandbox", - severity: "S1", - state: "work_in_progress", - assignee: "Tharindu A.", - assigneeIsMe: false, - slaClockType: "first_response", - minutesToBreach: -60, - createdAt: minutesAgo(60 * 10), - updatedAt: minutesAgo(60 * 2), - }, - { - id: "case-2002", - caseNumber: "CS-2002", - subject: "Choreo build hanging on private dep", - customer: "Stark Industries", - accountId: "acc-102", - projectId: "prj-stark-choreo", - projectName: "Choreo CI", - severity: "S2", - state: "open", - assignee: "Maya R.", - assigneeIsMe: false, - slaClockType: "ack", - minutesToBreach: 75, - createdAt: minutesAgo(60 * 3), - updatedAt: minutesAgo(60 * 3), - }, - { - id: "case-2003", - caseNumber: "CS-2003", - subject: "API rate limiting policy not applying", - customer: "Tyrell Corp", - accountId: "acc-103", - projectId: "prj-tyrell-apim", - projectName: "API Manager", - severity: "S3", - state: "work_in_progress", - // Paused: exercises the comment-gate disabled state in mock mode. - workState: "paused", - assignee: "Dilan W.", - assigneeIsMe: false, - slaClockType: "resolution", - minutesToBreach: 600, - createdAt: minutesAgo(60 * 24), - updatedAt: minutesAgo(60 * 4), - }, - { - id: "case-2004", - caseNumber: "CS-2004", - subject: "How to enable distributed tracing in IS?", - customer: "Stark Industries", - accountId: "acc-102", - projectId: "prj-stark-iam", - projectName: "IAM", - severity: "S4", - state: "closed", - assignee: "Priya N.", - assigneeIsMe: false, - slaClockType: "resolution", - minutesToBreach: 0, - createdAt: minutesAgo(60 * 100), - updatedAt: minutesAgo(60 * 40), - }, -]; - -/** - * Build the project/subscription-scoped WSO2 case reference (e.g. - * "ACMESUB-123") from the customer name + the numeric part of the case id. - * Illustrative only — the real value comes from the BE `wso2Id` field. - */ -function deriveWso2CaseId(seed: CaseSeed): string { - const prefix = (seed.customer.split(/\s+/)[0] ?? "WSO2") - .toUpperCase() - .replace(/[^A-Z0-9]/g, "") - .slice(0, 4); - const num = seed.id.replace(/\D/g, "") || "0"; - return `${prefix}SUB-${num}`; -} - -function hydrate(seed: CaseSeed): CsmCaseRow { - return { - ...seed, - // Default an in-progress case to `ongoing` so the comment composer is - // usable in mock mode; a seed may set `paused` explicitly. Non-in-progress - // cases have no work sub-state. - workState: - seed.workState ?? (seed.state === "work_in_progress" ? "ongoing" : null), - product: deriveProductName(seed.subject, seed.projectName), - wso2CaseId: deriveWso2CaseId(seed), - }; -} - -const ABT_CASES: CsmCaseRow[] = ABT_CASE_SEEDS.map(hydrate); -const ALL_EXTRA_CASES: CsmCaseRow[] = ALL_EXTRA_CASE_SEEDS.map(hydrate); - -/** - * The full seeded case set for a scope. Callers apply their own filtering and - * pagination (the cases hook slices this into pages; the dashboard tallies it). - */ -export function getMockCsmCases(scope: DashboardScope): CsmCaseRow[] { - return scope === "my_abt" ? ABT_CASES : [...ABT_CASES, ...ALL_EXTRA_CASES]; -} - -const ALL_CASES_BY_ID = new Map( - [...ABT_CASES, ...ALL_EXTRA_CASES].map((c) => [c.id, c]), -); - -const ALL_CASES_BY_NUMBER = new Map( - [...ABT_CASES, ...ALL_EXTRA_CASES].flatMap((c) => - c.caseNumber ? [[c.caseNumber, c] as [string, CsmCaseRow]] : [], - ), -); - -export function getMockCsmCaseById( - idOrNumber: string, -): CsmCaseRow | undefined { - return ALL_CASES_BY_ID.get(idOrNumber) ?? ALL_CASES_BY_NUMBER.get(idOrNumber); -} - -// --------------------------------------------------------------------------- -// Case detail decorations: deterministic per case id, so the same case keeps -// the same widget data across navigations. -// --------------------------------------------------------------------------- - -const CUSTOMER_CONTEXTS: Record = { - "acc-001": { - accountName: "Acme Financial", - tier: "enterprise", - region: "us-east-1", - primaryContact: "Renee Park", - primaryContactEmail: "renee.park@acmefinancial.com", - accountManager: "Lakshmi I.", - technicalOwner: "Chathura D.", - openCases: 6, - }, - "acc-002": { - accountName: "Globex Corp", - tier: "basic", - region: "eu-west-1", - primaryContact: "Helena Voss", - primaryContactEmail: "h.voss@globex.com", - accountManager: "Dilan W.", - openCases: 3, - }, - "acc-003": { - accountName: "Initech Systems", - tier: "enterprise", - region: "ap-southeast-1", - primaryContact: "Peter Gibbons", - primaryContactEmail: "peter@initech.io", - accountManager: "Lakshmi I.", - technicalOwner: "Tharindu A.", - openCases: 5, - }, - "acc-004": { - accountName: "Soylent Industries", - tier: "basic", - region: "us-west-2", - primaryContact: "Marie Sandwitch", - primaryContactEmail: "marie@soylent.com", - accountManager: "Maya R.", - openCases: 2, - }, - "acc-005": { - accountName: "Umbrella Health", - tier: "enterprise", - region: "us-east-1", - primaryContact: "Albert Wesker", - primaryContactEmail: "a.wesker@umbrella.health", - accountManager: "Priya N.", - openCases: 4, - }, - "acc-101": { - accountName: "Wayne Enterprises", - tier: "basic", - region: "us-east-1", - primaryContact: "Lucius Fox", - primaryContactEmail: "lfox@wayne.com", - accountManager: "Maya R.", - openCases: 1, - }, - "acc-102": { - accountName: "Stark Industries", - tier: "enterprise", - region: "us-west-1", - primaryContact: "Pepper Potts", - primaryContactEmail: "pepper@stark.com", - accountManager: "Dilan W.", - technicalOwner: "Asanka R.", - openCases: 4, - }, - "acc-103": { - accountName: "Tyrell Corp", - tier: "basic", - region: "eu-central-1", - primaryContact: "Eldon Tyrell", - primaryContactEmail: "eldon@tyrell.corp", - accountManager: "Priya N.", - openCases: 1, - }, -}; - -const FALLBACK_CUSTOMER: CaseCustomerContext = { - accountName: "Unknown Account", - tier: "basic", - region: "unknown", - primaryContact: "(no primary contact)", - primaryContactEmail: "—", - accountManager: "Unassigned", - openCases: 1, -}; - -// Map the coarse environment lane to a fixed-list deployment category. Prod -// deployments are treated as primary production in the mock. -const CATEGORY_FROM_ENV: Record< - CaseProductContext["environment"], - NonNullable -> = { - prod: "primary_production", - staging: "staging", - qa: "qa", - dev: "development", -}; - -function deriveProduct(c: CsmCaseRow): CaseProductContext { - const base = deriveProductBase(c); - return { ...base, deploymentCategory: CATEGORY_FROM_ENV[base.environment] }; -} - -function deriveProductBase(c: CsmCaseRow): CaseProductContext { - const subject = c.subject.toLowerCase(); - const project = c.projectName.toLowerCase(); - if (subject.includes("identity server") || project.includes("iam")) { - return { - product: "WSO2 Identity Server", - version: "7.1.0", - updateLevel: "wso2is-7.1.0.42", - deployment: c.projectName, - environment: "prod", - region: "us-east-1", - }; - } - if (project.includes("asgardeo") || subject.includes("asgardeo")) { - return { - product: "WSO2 Asgardeo", - version: "saas", - deployment: c.projectName, - environment: "prod", - region: "us-east-1", - }; - } - if (project.includes("api manager") || subject.includes("api manager")) { - return { - product: "WSO2 API Manager", - version: "4.4.0", - updateLevel: "wso2am-4.4.0.18", - deployment: c.projectName, - environment: "prod", - region: "eu-west-1", - }; - } - if (project.includes("choreo") || subject.includes("choreo")) { - return { - product: "WSO2 Choreo", - version: "saas", - deployment: c.projectName, - environment: "prod", - region: "us-east-1", - }; - } - if (project.includes("integrator") || project.includes("mi") || project.includes("streaming")) { - return { - product: "WSO2 Micro Integrator", - version: "4.4.0", - updateLevel: "wso2mi-4.4.0.7", - deployment: c.projectName, - environment: "staging", - region: "us-east-1", - }; - } - if (project.includes("open banking") || subject.includes("openbanking")) { - return { - product: "WSO2 Open Banking", - version: "4.0.0", - updateLevel: "wso2ob-4.0.0.21", - deployment: c.projectName, - environment: "prod", - region: "eu-west-1", - }; - } - return { - product: "WSO2 Platform", - version: "n/a", - deployment: c.projectName, - environment: "prod", - }; -} - -// SLA targets vary by severity. Numbers below are illustrative for the mock. -function deriveSlaClocks(c: CsmCaseRow): CaseSlaClock[] { - const ackTarget = c.severity === "S0" ? 15 : c.severity === "S1" ? 30 : c.severity === "S2" ? 60 : 240; - const frTarget = c.severity === "S0" ? 30 : c.severity === "S1" ? 60 : c.severity === "S2" ? 240 : 480; - const resTarget = - c.severity === "S0" ? 240 : c.severity === "S1" ? 480 : c.severity === "S2" ? 1440 : 4320; - - return [ - { - clockType: "ack", - // Ack is generally met once we've replied. Use the case's own clock - // to drive the active stage so the timeline matches the header chip. - state: c.slaClockType === "ack" ? (c.minutesToBreach < 0 ? "breached" : "running") : "met", - minutesToBreach: c.slaClockType === "ack" ? c.minutesToBreach : 0, - targetMinutes: ackTarget, - }, - { - clockType: "first_response", - state: - c.slaClockType === "first_response" - ? c.minutesToBreach < 0 - ? "breached" - : "running" - : c.slaClockType === "resolution" || c.state === "closed" - ? "met" - : "paused", - minutesToBreach: c.slaClockType === "first_response" ? c.minutesToBreach : 0, - targetMinutes: frTarget, - }, - { - clockType: "resolution", - state: - c.state === "closed" - ? "met" - : c.slaClockType === "resolution" - ? c.minutesToBreach < 0 - ? "breached" - : "running" - : "paused", - minutesToBreach: c.slaClockType === "resolution" ? c.minutesToBreach : resTarget, - targetMinutes: resTarget, - }, - ]; -} - -const ALL_WATCHERS: CaseWatcher[] = [ - { id: "w-1", name: "Sajith Ekanayaka", role: "wso2_engineer", isMe: true }, - { id: "w-2", name: "Lakshmi I.", role: "manager" }, - { id: "w-3", name: "Asanka R.", role: "wso2_engineer" }, - { id: "w-4", name: "Priya N.", role: "wso2_engineer" }, - { id: "w-5", name: "Renee Park", role: "customer_contact" }, - { id: "w-6", name: "Maya R.", role: "wso2_engineer" }, - { id: "w-7", name: "Dilan W.", role: "wso2_engineer" }, -]; - -function deriveWatchers(c: CsmCaseRow): CaseWatcher[] { - // Always include the assignee if known + 2-3 more deterministically. - const base: CaseWatcher[] = []; - if (c.assigneeIsMe) base.push({ id: "w-1", name: c.assignee, role: "wso2_engineer", isMe: true }); - else if (c.assignee !== "Unassigned") - base.push({ id: `w-assignee-${c.id}`, name: c.assignee, role: "wso2_engineer" }); - const seedIdx = parseInt(c.id.replace(/\D/g, ""), 10) || 0; - const pool = ALL_WATCHERS.filter((w) => !base.some((b) => b.name === w.name)); - const extras = [ - pool[seedIdx % pool.length], - pool[(seedIdx + 2) % pool.length], - pool[(seedIdx + 3) % pool.length], - ]; - const seen = new Set(base.map((w) => w.name)); - for (const w of extras) { - if (w && !seen.has(w.name)) { - base.push(w); - seen.add(w.name); - } - } - return base; -} - -function deriveLinkedItems(c: CsmCaseRow): CaseLinkedItem[] { - const seedIdx = parseInt(c.id.replace(/\D/g, ""), 10) || 0; - const linked: CaseLinkedItem[] = []; - - // Most cases get 1-2 related cases from the same account. - const sameAccount = [...ALL_CASES_BY_ID.values()].filter( - (other) => other.accountId === c.accountId && other.id !== c.id, - ); - for (let i = 0; i < Math.min(2, sameAccount.length); i += 1) { - const r = sameAccount[(seedIdx + i) % sameAccount.length]; - linked.push({ - id: `link-${c.id}-${r.id}`, - kind: "case", - reference: r.caseNumber ?? r.wso2CaseId ?? r.id, - title: r.subject, - state: r.state, - href: `/cases/${r.id}`, - }); - } - - // High-severity SaaS cases get a linked incident. - if ( - (c.severity === "S0" || c.severity === "S1") && - (c.projectName.toLowerCase().includes("choreo") || - c.projectName.toLowerCase().includes("asgardeo")) - ) { - linked.push({ - id: `link-${c.id}-inc`, - kind: "incident", - reference: `INC-${5000 + seedIdx}`, - title: "Regional latency event", - state: "investigating", - }); - } - - // Breached cases get an escalation. - if (c.minutesToBreach < 0 || c.severity === "S0") { - linked.push({ - id: `link-${c.id}-esc`, - kind: "escalation", - reference: `ESC-${300 + seedIdx}`, - title: "Customer-raised escalation", - state: "open", - }); - } - - // ~30% get a KB article. - if (seedIdx % 3 === 0) { - linked.push({ - id: `link-${c.id}-kb`, - kind: "kb", - reference: `KB-${10000 + seedIdx}`, - title: "Troubleshooting playbook", - state: "published", - }); - } - return linked; -} - -function deriveTags(c: CsmCaseRow): CaseTag[] { - const tags: CaseTag[] = []; - if (c.severity === "S0" || c.severity === "S1") - tags.push({ id: "t-priority", label: "high-priority", color: "error" }); - if (c.minutesToBreach < 0) - tags.push({ id: "t-breach", label: "sla-breached", color: "warning" }); - const subjLower = c.subject.toLowerCase(); - if (subjLower.includes("ldap")) tags.push({ id: "t-ldap", label: "ldap", color: "info" }); - if (subjLower.includes("saml") || subjLower.includes("oidc")) - tags.push({ id: "t-fed", label: "federation", color: "info" }); - if (subjLower.includes("latency") || subjLower.includes("oom")) - tags.push({ id: "t-perf", label: "performance", color: "primary" }); - if (subjLower.includes("certificate") || subjLower.includes("cert")) - tags.push({ id: "t-pki", label: "pki", color: "primary" }); - return tags; -} - -function deriveTimeLogs(c: CsmCaseRow): CaseTimeLogEntry[] { - const seedIdx = parseInt(c.id.replace(/\D/g, ""), 10) || 0; - const assignee = c.assignee === "Unassigned" ? "Sajith Ekanayaka" : c.assignee; - const totalMinutes = (seedIdx * 17) % 480 + 30; - const logs: CaseTimeLogEntry[] = [ - { - id: `tl-${c.id}-a`, - engineer: assignee, - hours: Math.round((totalMinutes / 60) * 4) / 4, - note: "Investigated logs, gathered customer-side traces.", - date: new Date(Date.now() - 60 * 60 * 1000 * 6).toISOString(), - }, - ]; - if (c.state !== "open") { - logs.unshift({ - id: `tl-${c.id}-b`, - engineer: assignee, - hours: 1.5, - note: "Reviewed customer config and reproduction steps.", - date: new Date(Date.now() - 60 * 60 * 1000 * 22).toISOString(), - }); - } - if (c.state === "solution_proposed" || c.state === "closed") { - logs.unshift({ - id: `tl-${c.id}-c`, - engineer: assignee, - hours: 2, - note: "Drafted proposed fix and verified in staging.", - date: new Date(Date.now() - 60 * 60 * 1000 * 2).toISOString(), - }); - } - return logs; -} - -function deriveAudit(c: CsmCaseRow): CaseAuditEntry[] { - const assignee = c.assignee === "Unassigned" ? "(unassigned)" : c.assignee; - const created: CaseAuditEntry = { - id: `a-${c.id}-0`, - kind: "created", - actor: "Customer", - description: `Case created at severity ${c.severity}`, - createdAt: c.createdAt, - }; - const events: CaseAuditEntry[] = [created]; - if (c.state !== "open") { - events.push({ - id: `a-${c.id}-1`, - kind: "assignee_change", - actor: "Routing", - description: `Assigned to ${assignee}`, - createdAt: new Date(new Date(c.createdAt).getTime() + 5 * 60_000).toISOString(), - }); - events.push({ - id: `a-${c.id}-2`, - kind: "state_change", - actor: assignee, - description: "Moved to Work in progress", - createdAt: new Date(new Date(c.createdAt).getTime() + 15 * 60_000).toISOString(), - }); - } - if (c.state === "awaiting_info") { - events.push({ - id: `a-${c.id}-3`, - kind: "state_change", - actor: assignee, - description: "Requested additional info from customer", - createdAt: c.updatedAt, - }); - } - if (c.state === "waiting_on_wso2") { - events.push({ - id: `a-${c.id}-3`, - kind: "state_change", - actor: assignee, - description: "Marked as waiting on internal WSO2 dependency", - createdAt: c.updatedAt, - }); - } - if (c.state === "solution_proposed") { - events.push({ - id: `a-${c.id}-3`, - kind: "state_change", - actor: assignee, - description: "Posted proposed solution to customer", - createdAt: c.updatedAt, - }); - } - if (c.state === "closed") { - events.push({ - id: `a-${c.id}-3`, - kind: "state_change", - actor: assignee, - description: "Case closed", - createdAt: c.updatedAt, - }); - } - if (c.minutesToBreach < 0 && c.state !== "closed") { - events.push({ - id: `a-${c.id}-breach`, - kind: "sla_breached", - actor: "System", - description: `${c.slaClockType.replace("_", " ")} SLA breached`, - createdAt: c.updatedAt, - }); - } - return events; -} - -const DESCRIPTIONS: Record = { - "case-1001": - `Hi WSO2 support, - -Token issuance latency on our corporate IdP has spiked to ~2.5s p95 since 09:30 UTC. This is impacting workforce SSO across all internal apps — engineers can't get into Jira, GitHub, or the deployment console. - -What we've checked so far: - - Mutual TLS handshake times look normal (~40ms) - - JDBC userstore connection pool sits at 18/100 — not exhausted - - GC pauses on IS nodes are under 50ms, heap usage at 45% - - No deployment on our side in the last 72 hours - - LDAP-backed userstore latency unchanged - -We tried rolling restart of the IS cluster at 10:15 UTC; latency dropped for ~3 minutes then climbed back. Attaching the access log slice from 09:00–10:30 UTC and a thread dump from is-prod-2 captured during the spike. - -Please treat as high priority — our CISO is asking for an ETA. - -Thanks, -Rohan Mehta -Platform Engineering, Acme Financial`, - "case-1002": - `Hi WSO2 team, - -API Manager gateway is returning intermittent 502s to roughly 3% of traffic during our 11:00–13:00 UTC peak window. Backend services behind the gateway are healthy (5xx-free, latency normal). Looks like the gateway itself is dropping connections. - -Observed pattern: - - 5xx rate climbs starting ~11:05 UTC, peaks around 12:20 UTC - - Two of three gateway pods show CPU at ~85% during the spike - - Heap usage steady at 60%, no OOM - - Suspect upstream connection pool saturation but we can't see pool metrics from outside - -Can you take a look at the gateway pod logs and pool config? We have the change window open if you need us to apply anything. - -Best regards, -Janet Park -Initech SRE`, - "case-1003": - `Hi WSO2 support, - -Our Micro Integrator cluster fails to start after we applied update level wso2mi-4.4.0.7 last night. Two pods stay in CrashLoopBackOff. - -The carbon log shows a ClassNotFoundException on org.wso2.carbon.security.user.api.UserStoreException coming from a custom security extension we built about a year ago. The extension was working fine on wso2mi-4.4.0.4. - -Can you tell us what changed in 4.4.0.7 around the user-API classloader? If it's a class move or rename, we can rebuild the extension; just need to know what to retarget. - -We're holding the rollout to other regions until we hear back. - -Thanks, -Bill Lumbergh -Initech Platform Team`, - "case-1004": - `Hi WSO2 support, - -The userinfo endpoint stopped returning the "groups" claim for users authenticated via our corporate IdP. This worked last week and we haven't changed our client config. - -Repro: - 1. Authenticate as bob@acmefinancial.com via the corporate IdP federation - 2. Exchange code for tokens at /oauth2/token - 3. Call /oauth2/userinfo with the access token - 4. Response includes sub, email, name — but no "groups" - -Expected: groups should be a JSON array of LDAP DN strings. - -Could a federated IdP attribute mapping have been changed, or is this a known issue in 7.1.0? Happy to provide a test user account if it helps. - -Best regards, -Rohan Mehta -Acme Financial`, - "case-1005": - `Hi support, - -Our Choreo deployment for the "checkout-v2" component has been stuck at "Provisioning" for about 45 minutes. The component is a Go service, build succeeded, but the pod never reaches Ready. - -We've already tried: - - Deleting and redeploying — same outcome - - Smaller resource ask (256Mi → 128Mi) — same outcome - - Switching the build profile — no effect - -There's nothing useful in the Choreo UI beyond "Provisioning". Could you check the underlying pod state? I'll attach kubectl describe output if you can pull it from the platform side. - -Thanks, -Daniel Owens -Engineering, Acme Choreo`, - "case-1015": - `Hi WSO2 support, - -Our Choreo runtime is going OOM under sustained load (~120 RPS sustained, no bursts). The crash takes the whole pod down and the autoscaler can't keep up — we're seeing ~40-minute cycles of healthy / OOM / new pod. - -Started this morning around 05:40 UTC right after the rollout of release v4.2.1. Reverting to v4.2.0 resolves it; we've confirmed the rollback on staging. - -Profile so far: - - All pods OOM under steady-state, no specific instance pattern - - 2Gi memory limit, 1Gi request, 1500m CPU limit - - Workload is 117–122 RPS for the last 6 hours of traffic - - Heap dump captured (heap-prod-01.hprof, ~480MB) — attaching - -Need this resolved or worked around urgently — our SLO budget for the month is ~70% spent already. - -Thanks, -Daniel Owens -Engineering, Acme Choreo`, - "case-1006": - `Hi WSO2 team, - -We're seeing the same SAML response signature validation error on the Asgardeo tenant for our staging Salesforce SP. The error in the IS logs is: - - org.wso2.carbon.identity.sso.saml.exception.IdentityException: Signature validation failed for SAML response - -This started after we rotated our Salesforce signing certificate yesterday. We re-uploaded the new cert into the SP configuration in Asgardeo. The new cert is RSA-2048, SHA-256 signed. - -Production tenant is still on the old cert and working fine; this is staging only. Can you check whether the cert metadata was actually picked up, or if there's a propagation delay we should wait through? - -Best regards, -Helena Voss -Globex IAM Team`, - "case-1011": - `Hi WSO2 support, - -We need help tuning the LDAP userstore for our IS deployment. Search queries against the userstore are coming back in 1.2–1.8s for queries that should be instant. We have ~85,000 user entries in the LDAP. - -What we've configured: - - ConnectionPoolingEnabled = true - - MaxActiveConnections = 50 - - ConnectionPoolMinIdle = 10 - - SearchScope = SUB on ou=people,dc=globex,dc=com - -Index status on the LDAP side looks fine — pres/sub/eq indexes on uid, mail, cn, member. Network RTT to the LDAP from IS pods is consistent at ~2ms. - -Suspect we're paginating poorly or doing redundant searches. Can you advise on the right pooling config + tracing the actual LDAP queries IS is firing? - -Thanks, -Marcus Liang -Globex Security Engineering`, -}; - -function describe(c: CsmCaseRow): string { - const seeded = DESCRIPTIONS[c.id]; - if (seeded) return seeded; - - // Fallback template: greeting + body + sign-off so every case reads like - // a real ticket even without a hand-written description. The greeting is - // intentionally generic — the customer doesn't know who will pick up the - // case at the time of creation. - const customerFirst = c.customer.split(/\s+/)[0] ?? c.customer; - return [ - `Hi WSO2 support,`, - "", - `${c.subject} — reported on project ${c.projectName}.`, - "", - "What we're observing:", - ` - Issue first noticed earlier today during normal operations`, - ` - Severity ${c.severity} based on customer impact`, - ` - Workaround status: investigating, no permanent mitigation yet`, - "", - "Additional reproduction details and logs are captured in the comment thread below. Happy to jump on a call if it helps move things along.", - "", - "Thanks,", - `${customerFirst}`, - `${c.customer}`, - ].join("\n"); -} - -function deriveAttachments(c: CsmCaseRow): CaseAttachment[] { - const seedIdx = parseInt(c.id.replace(/\D/g, ""), 10) || 0; - const sets: Record = { - 0: [ - { - id: `att-${c.id}-1`, - filename: "thread-dump.log", - size: 154_321, - contentType: "text/plain", - uploadedBy: c.customer.split(" ")[0], - uploadedAt: new Date(Date.now() - 60 * 60 * 1000 * 5).toISOString(), - }, - ], - 1: [ - { - id: `att-${c.id}-1`, - filename: "gw-pod-metrics.csv", - size: 21_440, - contentType: "text/csv", - uploadedBy: c.customer.split(" ")[0], - uploadedAt: new Date(Date.now() - 60 * 60 * 1000 * 3).toISOString(), - }, - { - id: `att-${c.id}-2`, - filename: "carbon.log", - size: 982_133, - contentType: "text/plain", - uploadedBy: c.assignee === "Unassigned" ? "Sajith Ekanayaka" : c.assignee, - uploadedAt: new Date(Date.now() - 60 * 60 * 1000 * 2).toISOString(), - }, - ], - 2: [ - { - id: `att-${c.id}-1`, - filename: "screenshot.png", - size: 312_558, - contentType: "image/png", - uploadedBy: c.customer.split(" ")[0], - uploadedAt: new Date(Date.now() - 60 * 60 * 1000 * 1.2).toISOString(), - }, - ], - 3: [], - }; - return sets[seedIdx % 4] ?? []; -} - -// Attachments uploaded during a mock session, keyed by case id, so a mock -// upload is visible on the next list refetch (the seeded set is deterministic). -const mockUploadedAttachments = new Map(); - -/** Mock attachment list: seeded set plus anything uploaded this session. */ -export function getMockCsmCaseAttachments(caseId: string): CaseAttachment[] { - const row = getMockCsmCaseById(caseId); - const seeded = row ? deriveAttachments(row) : []; - const uploaded = mockUploadedAttachments.get(caseId) ?? []; - // Newest first matches how the widget sorts; concat is enough since the - // widget re-sorts by uploadedAt. - return [...uploaded, ...seeded]; -} - -/** Mock attachment upload: records and echoes back a fresh `CaseAttachment`. */ -export function postMockCsmCaseAttachment(input: { - caseId: string; - filename: string; - size: number; - contentType: string; - uploadedBy: string; -}): CaseAttachment { - const created: CaseAttachment = { - id: `att-${input.caseId}-${Date.now()}`, - filename: input.filename, - size: input.size, - contentType: input.contentType, - uploadedBy: input.uploadedBy, - uploadedAt: new Date().toISOString(), - }; - const existing = mockUploadedAttachments.get(input.caseId) ?? []; - mockUploadedAttachments.set(input.caseId, [created, ...existing]); - return created; -} - -/** - * Mirror of the backend transition graph in - * `apps/csm-portal/backend/internal/handler/state.go` (`nextStates`). The mock - * stands in for the backend, so it owns this copy the way the real backend does; - * the webapp itself never re-derives the graph — it renders from `nextStates`. - */ -const MOCK_NEXT_STATES: Record = { - open: ["work_in_progress"], - work_in_progress: [ - "waiting_on_wso2", - "awaiting_info", - "solution_proposed", - "closed", - ], - waiting_on_wso2: ["work_in_progress"], - awaiting_info: ["waiting_on_wso2"], - solution_proposed: ["closed", "waiting_on_wso2"], - closed: [], -}; - -export function getMockCsmCaseDetailById( - idOrNumber: string, -): CsmCaseDetail | undefined { - const row = getMockCsmCaseById(idOrNumber); - if (!row) return undefined; - const customerContext = CUSTOMER_CONTEXTS[row.accountId] ?? FALLBACK_CUSTOMER; - return { - ...row, - nextStates: MOCK_NEXT_STATES[row.state] ?? [], - description: describe(row), - assignmentGroup: row.projectName.toLowerCase().includes("choreo") - ? "grp.choreo_sre" - : row.projectName.toLowerCase().includes("asgardeo") - ? "grp.asgardeo_sre" - : "grp.cre_team", - createdBy: customerContext.primaryContact, - customerContext, - productContext: deriveProduct(row), - slaClocks: deriveSlaClocks(row), - watchers: deriveWatchers(row), - linkedItems: deriveLinkedItems(row), - tags: deriveTags(row), - timeLogs: deriveTimeLogs(row), - audit: deriveAudit(row), - attachments: deriveAttachments(row), - isWatching: row.assigneeIsMe, - }; -} diff --git a/apps/csm-portal/webapp/src/features/csm-cases/api/mocks/commentsMocks.ts b/apps/csm-portal/webapp/src/features/csm-cases/api/mocks/commentsMocks.ts deleted file mode 100644 index 075e4bc84e..0000000000 --- a/apps/csm-portal/webapp/src/features/csm-cases/api/mocks/commentsMocks.ts +++ /dev/null @@ -1,787 +0,0 @@ -// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -import type { CsmCaseComment } from "@features/csm-cases/types/csmCases"; - -const minutesAgo = (n: number): string => - new Date(Date.now() - n * 60_000).toISOString(); - -const SEED_COMMENTS: Record = { - "case-1001": [ - { - id: "cmt-1001-1", - caseId: "case-1001", - authorName: "Rohan Mehta", - authorRole: "customer", - bodyHtml: - "

Hi WSO2 support,

" + - "

P99 token issuance latency on prod jumped from ~80ms to ~1.2s starting around 09:15 UTC. No deploy on our side in the last 72 hours.

" + - "

Quick summary of what we've ruled out:

" + - "
    " + - "
  • GC pauses on all IS nodes are under 50ms
  • " + - "
  • Mutual TLS handshake times look normal (~40ms)
  • " + - "
  • JDBC userstore pool sits at 18/100 — not exhausted
  • " + - "
  • LDAP-backed userstore latency unchanged
  • " + - "
" + - "

For reference, our last green run is in this Grafana snapshot: is-token-latency (last 7d).

" + - "

I've attached the IS access log slice from 09:00–10:30 UTC and a thread dump captured during the spike. Treating as high priority — our CISO is asking for an ETA.

" + - "

Thanks,
Rohan Mehta
Platform Engineering, Acme Financial

", - createdAt: minutesAgo(60 * 7), - }, - { - id: "cmt-1001-2", - caseId: "case-1001", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Hi Rohan,

" + - "

Picked this up. A few clarifying questions before I dig into the thread dump:

" + - "
    " + - "
  1. Does the spike show up on the LDAP-backed userstore or only the JDBC primary?
  2. " + - "
  3. Are sessions persisting across cluster nodes (sticky sessions vs. distributed cache)?
  4. " + - "
  5. Have any TLS truststore or KMS configs been updated on the load balancer in front of IS?
  6. " + - "
" + - "

While you're checking — we documented a similar token-issuance-latency investigation in our IS troubleshooting playbook. The connection pool exhaustion section is what I'm comparing your numbers against.

" + - "

Will follow up with the thread-dump analysis shortly.

" + - "

Best regards,
Sajith

", - createdAt: minutesAgo(60 * 6), - }, - { - id: "cmt-1001-3", - caseId: "case-1001", - authorName: "Rohan Mehta", - authorRole: "customer", - bodyHtml: - "

Hi Sajith,

" + - "

Answers:

" + - "
    " + - "
  1. Only the JDBC primary. LDAP-backed userstore latency is unchanged at ~30ms.
  2. " + - "
  3. Sticky sessions on the LB; distributed cache (Hazelcast) is enabled across the cluster.
  4. " + - "
  5. No truststore or KMS changes on the LB or on IS in the last 30 days. Last network change was 2 weeks ago — added a WAF rule, nothing in the IS path.
  6. " + - "
" + - "

Two more data points we noticed:

" + - "
Hazelcast cluster log shows ~12 partition migrations between 09:00 and 09:30 UTC. We don't usually see that during steady-state operation.
" + - "

Could that be connected?

" + - "

Thanks,
Rohan

", - createdAt: minutesAgo(60 * 5 + 30), - }, - { - id: "cmt-1001-4", - caseId: "case-1001", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Hi Rohan,

" + - "

That Hazelcast detail is exactly what I needed. The thread dump confirms it — the IS auth threads are blocked on HazelcastInstance.getMap(\"session-cache\") waiting for partition stabilisation. Snippet from is-prod-2:

" + - "
\"http-nio-9443-exec-127\" #847 daemon\n  at com.hazelcast.spi.impl.operationservice.impl.Invocation_Future.get(Invocation_Future.java:71)\n  at com.hazelcast.map.impl.proxy.MapProxyImpl.getInternal(MapProxyImpl.java:158)\n  at org.wso2.carbon.identity.session.SessionDataStore.getSessionContextData(SessionDataStore.java:412)\n  at org.wso2.carbon.identity.oauth2.token.TokenIssuanceHandler.handle(TokenIssuanceHandler.java:289)
" + - "

The 12 partition migrations match a known Hazelcast issue when the cluster sees a transient network hiccup and re-balances under load. See product-is#18234 for the upstream context.

" + - "

Two paths forward:

" + - "
    " + - "
  • Immediate workaround: raise hazelcast.client.invocation.timeout.seconds from default 120s to 240s on the IS nodes, and increase partition.migration.commit.timeout.seconds to 60s. This won't prevent migrations but it will stop them from cascading into request timeouts.
  • " + - "
  • Permanent fix: upgrade to wso2is-7.1.0.43 (released yesterday) which includes the Hazelcast 5.3.6 client with the fix for #18234.
  • " + - "
" + - "

Let me know which path you want to take. Happy to hop on a call to walk through the workaround config if it helps.

" + - "

Best regards,
Sajith

", - createdAt: minutesAgo(60 * 4), - }, - { - id: "cmt-1001-5", - caseId: "case-1001", - authorName: "Rohan Mehta", - authorRole: "customer", - bodyHtml: - "

Hi Sajith,

" + - "

Going with the workaround for now. Change window opens at 15:30 UTC — we'll apply the Hazelcast config bump first, monitor for an hour, then plan the .43 upgrade for next week's maintenance.

" + - "

Will report back. Appreciate the fast turnaround.

" + - "

Thanks,
Rohan

", - createdAt: minutesAgo(60 * 3 + 15), - }, - { - id: "cmt-1001-6", - caseId: "case-1001", - authorName: "System", - authorRole: "system", - bodyHtml: "

First-response SLA breached by 22 minutes.

", - createdAt: minutesAgo(22), - }, - ], - "case-1002": [ - { - id: "cmt-1002-1", - caseId: "case-1002", - authorName: "Janet Park", - authorRole: "customer", - bodyHtml: - "

Hi WSO2 team,

" + - "

API Manager gateway is returning intermittent 502s to roughly 3% of traffic during the 12:00–13:00 UTC peak window. Backend services behind the gateway are healthy:

" + - "
    " + - "
  • Upstream 5xx rate: 0.01% (unchanged)
  • " + - "
  • Upstream p99 latency: 180ms (normal)
  • " + - "
  • No backend timeouts in upstream logs
  • " + - "
" + - "

Failure pattern on the gateway side:

" + - "
Two of three gateway pods show CPU at ~85% during the spike. The third stays at 40%. Pod with low CPU is the newest — restarted 3 hours ago.
" + - "

Suspect upstream connection pool saturation but we can't see pool metrics from outside. Change window is open if you need us to apply anything. Reference dashboard: apim-gateway-24h.

" + - "

Best regards,
Janet Park
Initech SRE

", - createdAt: minutesAgo(60 * 3), - }, - { - id: "cmt-1002-2", - caseId: "case-1002", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Hi Janet,

" + - "

On it. Two asks while I get our team looking at the dashboard:

" + - "
    " + - "
  1. Thread dump from one of the high-CPU gateway pods captured during the spike (not after). jstack <pid> > gw-pod-dump.txt works.
  2. " + - "
  3. Current values for these from the gateway deployment.toml:" + - "
    [transport.http.properties]\nthread.pool.size = 200\nmax.connections.per.host = 100\nconnection.keep.alive = true
    " + - "
  4. " + - "
" + - "

The low-CPU pod being the freshly-restarted one is a strong hint that the pool is leaking. We've seen this pattern in 4.3.x; see api-manager#14087.

" + - "

Thanks,
Sajith

", - createdAt: minutesAgo(60 * 2 + 20), - }, - { - id: "cmt-1002-3", - caseId: "case-1002", - authorName: "Janet Park", - authorRole: "customer", - bodyHtml: - "

Hi Sajith,

" + - "

Thread dump and configs uploaded. The relevant transport block:

" + - "
[transport.http.properties]\nthread.pool.size = 200\nmax.connections.per.host = 100\nconnection.keep.alive = true\nsocket.timeout = 60000\nconnection.timeout = 5000
" + - "

From the thread dump on apim-gw-2 (high CPU):

" + - "
92 threads stuck in PoolingHttpClientConnectionManager.lease(). About 60 of those are blocked on BlockingChannel.read() waiting for a connection.
" + - "

This matches the pool exhaustion hypothesis. Should we just bump max.connections.per.host or is there a smarter fix?

" + - "

Thanks,
Janet

", - createdAt: minutesAgo(60 * 1 + 50), - }, - { - id: "cmt-1002-4", - caseId: "case-1002", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Hi Janet,

" + - "

That's the exact pattern from #14087. Two recommended changes — both safe to apply hot:

" + - "
    " + - "
  1. Raise max.connections.per.host from 100 to 500. Your traffic profile justifies it; default of 100 was calibrated for low-RPS deployments.
  2. " + - "
  3. Set connection.eviction.idle.timeout = 30000 to actively reap stale connections. The default leak path keeps half-open connections around for 60+s.
  4. " + - "
" + - "

After applying, restart pods one at a time and watch:

" + - "
kubectl rollout restart deployment/apim-gateway -n apim\nkubectl logs -f -l app=apim-gateway -n apim | grep -E 'lease|eviction'
" + - "

Marking solution_proposed once you confirm. If steady-state is still bumpy after 24h we can dig into the leak source (looks like a custom mediator holding refs, but let's see if the config fix is enough first).

" + - "

Best regards,
Sajith

", - createdAt: minutesAgo(60 + 30), - }, - { - id: "cmt-1002-5", - caseId: "case-1002", - authorName: "Janet Park", - authorRole: "customer", - bodyHtml: - "

Hi Sajith,

" + - "

Applied both changes 30 minutes ago. Rolling restart complete. Gateway CPU dropped from 85% to 45% across all pods, 5xx rate at 0.02% (effectively gone). Will keep monitoring for the next peak window and report back tomorrow.

" + - "

Thanks,
Janet

", - createdAt: minutesAgo(4), - }, - ], - "case-1003": [ - { - id: "cmt-1003-1", - caseId: "case-1003", - authorName: "Bill Lumbergh", - authorRole: "customer", - bodyHtml: - "

Hi WSO2 support,

" + - "

Our MI cluster fails to start after applying update level wso2mi-4.4.0.7 last night. Two pods stay in CrashLoopBackOff.

" + - "

The Carbon log shows:

" + - "
org.osgi.framework.BundleException: Could not resolve module: com.initech.security.userstore [318]\n  Caused by: java.lang.ClassNotFoundException: \n    org.wso2.carbon.security.user.api.UserStoreException
" + - "

This is from a custom security extension we built a year ago. It worked fine on wso2mi-4.4.0.4 through 4.4.0.6.

" + - "

What changed in 4.4.0.7 around the user-API classloader? If you can confirm a class move/rename we'll just rebuild the extension.

" + - "

Best regards,
Bill Lumbergh
Initech Platform Team

", - createdAt: minutesAgo(60 * 6), - }, - { - id: "cmt-1003-wn-1", - caseId: "case-1003", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - internal: true, - bodyHtml: - "

Work note: 4.4.0.7 included a JDK 17 cleanup that moved org.wso2.carbon.security.user.api from the old org.wso2.carbon.core bundle into org.wso2.carbon.identity.framework. Customer's extension is targeting the old import-package. Need to give them the new manifest entries.

", - createdAt: minutesAgo(60 * 5 + 30), - }, - { - id: "cmt-1003-2", - caseId: "case-1003", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Hi Bill,

" + - "

Confirmed — 4.4.0.7 moved the user-API classes during a JDK 17 cleanup. Your extension is importing the old bundle.

" + - "

Old (broken) import in your MANIFEST.MF:

" + - "
Import-Package: org.wso2.carbon.user.api;version=\"[1.0,2)\"
" + - "

New entry to add:

" + - "
Import-Package: \n  org.wso2.carbon.identity.user.api;version=\"[1.0,2)\",\n  org.wso2.carbon.user.api;version=\"[1.0,2)\";resolution:=optional
" + - "

The resolution:=optional on the old package keeps your extension backward-compatible with 4.4.0.6 and earlier — same JAR will load on both.

" + - "

Full migration notes: MI 4.4 JDK 17 package changes.

" + - "

Let me know when you've rebuilt and we can verify in staging.

" + - "

Best regards,
Sajith

", - createdAt: minutesAgo(60 * 5), - }, - { - id: "cmt-1003-3", - caseId: "case-1003", - authorName: "Bill Lumbergh", - authorRole: "customer", - bodyHtml: - "

Hi Sajith,

" + - "

That worked. Rebuilt the extension with the dual-import, deployed to staging on 4.4.0.7 — cluster came up clean, all health checks green. Also tested on 4.4.0.6 to confirm backward compatibility, no regression.

" + - "

Planning to roll to prod tonight during the change window. Thanks for the fast turnaround.

" + - "

Thanks,
Bill

", - createdAt: minutesAgo(60 * 1 + 30), - }, - ], - "case-1004": [ - { - id: "cmt-1004-1", - caseId: "case-1004", - authorName: "Rohan Mehta", - authorRole: "customer", - bodyHtml: - "

Hi support,

" + - "

The userinfo endpoint stopped returning the groups claim for users authenticated via our corporate IdP. Worked last week, no client config change on our side.

" + - "

Repro:

" + - "
    " + - "
  1. Authenticate as bob@acmefinancial.com via the corporate IdP federation
  2. " + - "
  3. Exchange code for tokens at /oauth2/token
  4. " + - "
  5. Call /oauth2/userinfo with the access token
  6. " + - "
  7. Response includes sub, email, name — but no groups
  8. " + - "
" + - "

Expected: groups should be a JSON array of LDAP DN strings.

" + - "

Reference for our claim mapping: OIDC claims mapping docs.

" + - "

Could a federated IdP attribute mapping have been changed, or is this a known issue in 7.1.0? Test account available on request.

" + - "

Best regards,
Rohan Mehta
Acme Financial

", - createdAt: minutesAgo(75), - }, - { - id: "cmt-1004-2", - caseId: "case-1004", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Hi Rohan,

" + - "

Picked up. While I dig in, can you check whether the groups claim appears in the ID token vs. just the userinfo response?

" + - "
# Decode the ID token from /oauth2/token response\necho \"<id_token>\" | cut -d. -f2 | base64 -d | jq .
" + - "

If groups shows up in the ID token but not userinfo, that's a claim-scope-mapping issue on the userinfo endpoint. If it's missing from both, it's an attribute-mapping issue on the federated authenticator.

" + - "

Thanks,
Sajith

", - createdAt: minutesAgo(60), - }, - ], - "case-1005": [ - { - id: "cmt-1005-1", - caseId: "case-1005", - authorName: "Daniel Owens", - authorRole: "customer", - bodyHtml: - "

Hi WSO2 team,

" + - "

Our Choreo deployment for the checkout-v2 component has been stuck at \"Provisioning\" for about 45 minutes. Component is a Go service, build succeeded, but the pod never reaches Ready.

" + - "

Tried already:

" + - "
    " + - "
  • Deleting and redeploying — same outcome
  • " + - "
  • Smaller resource ask (256Mi → 128Mi) — same outcome
  • " + - "
  • Switching the build profile — no effect
  • " + - "
" + - "

Nothing useful in the Choreo UI beyond \"Provisioning\". Can you check the underlying pod state from the platform side?

" + - "

Thanks,
Daniel Owens
Engineering, Acme Choreo

", - createdAt: minutesAgo(60 * 4), - }, - { - id: "cmt-1005-wn-1", - caseId: "case-1005", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - internal: true, - bodyHtml: - "

Work note: Suspect the new ingress controller default timeouts. Will pair with @Priya on tomorrow's call before sharing anything externally.

", - createdAt: minutesAgo(60 * 3 + 30), - }, - { - id: "cmt-1005-2", - caseId: "case-1005", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Hi Daniel,

" + - "

Pulled the pod state. The container is alive but failing the readiness probe — TCP check on port 8080 times out:

" + - "
Events:\n  Warning  Unhealthy  43m (x12)  kubelet  Readiness probe failed: \n    dial tcp 10.4.7.23:8080: i/o timeout
" + - "

What port is the Go service binding to? In the Choreo component descriptor (the .choreo/component.yaml), the default expected port is 8080. If your service binds to anything else (3000, 9000, etc.) the probe will never pass.

" + - "

Reference: Choreo component endpoint config.

" + - "

Thanks,
Sajith

", - createdAt: minutesAgo(60 * 2 + 50), - }, - { - id: "cmt-1005-3", - caseId: "case-1005", - authorName: "Daniel Owens", - authorRole: "customer", - bodyHtml: - "

Hi Sajith,

" + - "

Found it. The service binds to :9001 in the Go code, but our component.yaml still has the scaffold default:

" + - "
endpoints:\n  - name: checkout\n    port: 8080  # <-- wrong\n    type: REST
" + - "

Updating to 9001, rebuilding. Will close this out if the next deploy succeeds.

" + - "

Thanks for the quick turnaround.

" + - "

Thanks,
Daniel

", - createdAt: minutesAgo(60 * 2), - }, - { - id: "cmt-1005-4", - caseId: "case-1005", - authorName: "Daniel Owens", - authorRole: "customer", - bodyHtml: - "

Deploy succeeded with the corrected port. Component is healthy. Closing.

" + - "

Thanks,
Daniel

", - createdAt: minutesAgo(60), - }, - ], - - "case-1006": [ - { - id: "cmt-1006-1", - caseId: "case-1006", - authorName: "Helena Voss", - authorRole: "customer", - bodyHtml: - "

Hi WSO2 team,

" + - "

SAML response signature validation is failing on our Asgardeo tenant for the staging Salesforce SP. Error from the IS logs:

" + - "
org.wso2.carbon.identity.sso.saml.exception.IdentityException:\n  Signature validation failed for SAML response\n  at org.wso2.carbon.identity.sso.saml.processors.SPInitSSOAuthnRequestValidator\n    .validateSignature(SPInitSSOAuthnRequestValidator.java:331)
" + - "

Started after we rotated the Salesforce signing cert yesterday. New cert is RSA-2048, SHA-256, re-uploaded to the SP config in Asgardeo. Production tenant is on the old cert and working fine; staging only.

" + - "

Reference: SF cert rotation docs.

" + - "

Best regards,
Helena Voss
Globex IAM Team

", - createdAt: minutesAgo(60 * 8), - }, - { - id: "cmt-1006-2", - caseId: "case-1006", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Hi Helena,

" + - "

Two things to verify:

" + - "
    " + - "
  1. The cert thumbprint stored on the SP record actually matches what you uploaded. From the Asgardeo console: SP > Inbound auth > SAML > Certificate — copy the displayed thumbprint.
  2. " + - "
  3. The signature algorithm on the request matches what's configured. Staging SF may be sending SHA-1 even after the rotation; the SP record needs to accept it.
  4. " + - "
" + - "

Quick check from your side — paste the relevant block from the SP record (you can redact the cert body, just keep the algorithm fields):

" + - "
<ds:SignatureMethod Algorithm=\"...\"/>\n<ds:DigestMethod Algorithm=\"...\"/>
" + - "

Thanks,
Sajith

", - createdAt: minutesAgo(60 * 7), - }, - { - id: "cmt-1006-3", - caseId: "case-1006", - authorName: "Helena Voss", - authorRole: "customer", - bodyHtml: - "

Hi Sajith,

" + - "

Thumbprint on the SP record matches the new cert exactly. From the SAML config:

" + - "
<ds:SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\"/>\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"/>
" + - "

Both SHA-256. The request from SF staging also uses SHA-256 (I checked the raw POST).

" + - "

One thing I noticed in the IS logs:

" + - "
The cert thumbprint in the failed validation log shows the OLD cert thumbprint, not the new one we uploaded.
" + - "

Cache?

" + - "

Thanks,
Helena

", - createdAt: minutesAgo(60 * 6), - }, - { - id: "cmt-1006-4", - caseId: "case-1006", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Hi Helena,

" + - "

Yep — Asgardeo caches the SP cert for 1 hour by default. Your upload landed but validation is still hitting the old cached cert. Two options:

" + - "
    " + - "
  • Wait it out — cache TTL expires within the hour, then re-test.
  • " + - "
  • Force refresh — toggle the SP's Enable signature validation off + save, then back on + save. This invalidates the cache entry.
  • " + - "
" + - "

Production wasn't affected because cert was rotated during a window where the cache happened to be cold.

" + - "

Background: Asgardeo SP cert cache reference.

" + - "

Best regards,
Sajith

", - createdAt: minutesAgo(60 * 5), - }, - { - id: "cmt-1006-5", - caseId: "case-1006", - authorName: "Helena Voss", - authorRole: "customer", - bodyHtml: - "

Hi Sajith,

" + - "

Force-refreshed via the toggle. Validation works immediately. Closing this out.

" + - "

Thanks,
Helena

", - createdAt: minutesAgo(60 * 4 + 30), - }, - ], - "case-1011": [ - { - id: "cmt-1011-1", - caseId: "case-1011", - authorName: "Marcus Liang", - authorRole: "customer", - bodyHtml: - "

Hi WSO2 team,

" + - "

Need help tuning the LDAP userstore on our IS deployment. Search queries against the userstore are taking 1.2–1.8s for queries that should be instant. We have ~85,000 user entries.

" + - "

Current config (from user-mgt.xml):

" + - "
<Property name=\"ConnectionPoolingEnabled\">true</Property>\n<Property name=\"MaxActiveConnections\">50</Property>\n<Property name=\"ConnectionPoolMinIdle\">10</Property>\n<Property name=\"SearchScope\">SUB</Property>\n<Property name=\"UserSearchBase\">ou=people,dc=globex,dc=com</Property>
" + - "

LDAP-side indexes look fine — pres/sub/eq on uid, mail, cn, member. Network RTT to LDAP is stable at ~2ms.

" + - "

Suspect we're paginating poorly or doing redundant searches. Reference doc we've been working from: userstore tuning guide.

" + - "

Thanks,
Marcus Liang
Globex Security Engineering

", - createdAt: minutesAgo(60 * 10), - }, - { - id: "cmt-1011-2", - caseId: "case-1011", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Hi Marcus,

" + - "

The pool config looks fine for 85k entries. Two likely culprits:

" + - "
    " + - "
  1. Group lookup amplification — IS hits the LDAP for the user, then for each group the user is in, then for each user in those groups (when role-based authorisation is enabled). Easy to verify; enable LDAP-side query logging for 5 minutes during a normal load period and count queries per login.
  2. " + - "
  3. SearchScope=SUB from a deep base DN walks the whole subtree. If your tree has many OUs under ou=people, this is slow even with indexes.
  4. " + - "
" + - "

Can you share:

" + - "
    " + - "
  • A 1-min LDAP query log slice during peak (sanitised)
  • " + - "
  • The OU layout under ou=people,dc=globex,dc=com
  • " + - "
" + - "

Best regards,
Sajith

", - createdAt: minutesAgo(60 * 9 + 30), - }, - { - id: "cmt-1011-3", - caseId: "case-1011", - authorName: "Marcus Liang", - authorRole: "customer", - bodyHtml: - "

Hi Sajith,

" + - "

You called it. Query log shows 47 LDAP queries per login on average. The amplification pattern is clear:

" + - "
1 bind + 1 user search + 3 group memberOf lookups + 12 nested group enumerations + 30 attribute fetches per nested member.
" + - "

OU layout is flat — everyone under ou=people directly, no sub-OUs. So SUB scope shouldn't be the issue, but the nested group walk definitely is.

" + - "

How do we cut the amplification?

" + - "

Thanks,
Marcus

", - createdAt: minutesAgo(60 * 8 + 30), - }, - { - id: "cmt-1011-4", - caseId: "case-1011", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Hi Marcus,

" + - "

Three tweaks that compound to cut your per-login query count from ~47 to ~5:

" + - "
    " + - "
  1. Enable group caching on the userstore:" + - "
    <Property name=\"GroupListCacheEnabled\">true</Property>\n<Property name=\"GroupListCacheTimeout\">300</Property>
    " + - "5-minute TTL. Drops repeated group enumeration calls.
  2. " + - "
  3. Cap nested group depth. Default is unlimited; for most enterprise directories depth 3 is the natural ceiling:" + - "
    <Property name=\"MaxRoleNamesLimit\">100</Property>\n<Property name=\"NestedGroupSearchDepth\">3</Property>
  4. " + - "
  5. Switch to memberOf-based group resolution. If your LDAP supports the memberOf reverse-index (RFC 4511 attribute), single-query group lookup replaces the recursive walk:" + - "
    <Property name=\"ReadGroups\">true</Property>\n<Property name=\"GroupMembershipAttribute\">memberOf</Property>\n<Property name=\"BackLinksEnabled\">true</Property>
  6. " + - "
" + - "

Apply 1 + 2 first (safe, no LDAP-side requirements). 3 needs your AD/LDAP admin to confirm memberOf is populated — most Active Directory deployments have it on by default. OpenLDAP needs the memberof overlay.

" + - "

Restart IS after applying. Watch org.wso2.carbon.user.core.ldap at DEBUG for 10 min to confirm the query count drops.

" + - "

Best regards,
Sajith

", - createdAt: minutesAgo(60 * 7), - }, - { - id: "cmt-1011-5", - caseId: "case-1011", - authorName: "Marcus Liang", - authorRole: "customer", - bodyHtml: - "

Hi Sajith,

" + - "

Applied 1 + 2 in staging. Per-login query count dropped from 47 to 12. Login latency for the test users came down from 1.6s to ~280ms.

" + - "

Will check with our LDAP admin on memberOf overlay for #3 (we run OpenLDAP), but 280ms is already inside our SLO. Rolling 1 + 2 to prod tomorrow.

" + - "

Thanks for the deep dive.

" + - "

Thanks,
Marcus

", - createdAt: minutesAgo(60 * 5), - }, - ], - - // ------------------------------------------------------------------------ - // case-1015: Long-running technical thread used to evaluate engineer UX on - // dense comment trails — Java stack traces, code blocks, config diffs, - // long-form work notes. Spans ~36 hours of back-and-forth. - // ------------------------------------------------------------------------ - "case-1015": [ - { - id: "cmt-1015-1", - caseId: "case-1015", - authorName: "Daniel Owens", - authorRole: "customer", - bodyHtml: - "

Choreo runtime is going OOM under sustained load (~120 RPS sustained). The crash takes the whole pod down and the autoscaler can't keep up. Started this morning around 05:40 UTC after the rollout of release v4.2.1. Reverting to v4.2.0 resolves it.

Attaching the heap dump (heap-prod-01.hprof, ~480MB) and the last 2000 lines of the runtime log.

", - createdAt: minutesAgo(60 * 35), - }, - { - id: "cmt-1015-2", - caseId: "case-1015", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Hi Daniel — picked this up. Can you confirm:

  1. Are all pods OOMing, or just specific ones?
  2. What's the resources.limits.memory on the deployment?
  3. Is the workload steady-state ~120 RPS, or are there bursts above that?

Pulling the heap dump now — will analyze with Eclipse MAT and report back.

", - createdAt: minutesAgo(60 * 34 + 40), - }, - { - id: "cmt-1015-3", - caseId: "case-1015", - authorName: "Daniel Owens", - authorRole: "customer", - bodyHtml: - "

1) All pods, every ~40min under steady load.

2) Limits are:

resources:\n  limits:\n    memory: 2Gi\n    cpu: 1500m\n  requests:\n    memory: 1Gi\n    cpu: 500m

3) Steady-state, no bursts. The dashboard confirms 117–122 RPS for the last 6 hours.

", - createdAt: minutesAgo(60 * 34 + 20), - }, - { - id: "cmt-1015-wn-1", - caseId: "case-1015", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - internal: true, - bodyHtml: - "

Work note: Heap dump downloaded. MAT leak suspect points at io.netty.buffer.PooledByteBufAllocator retaining ~1.4GB. Could be the same Netty pool leak we saw in INC-4877 (Choreo runtime 4.1.x). Will diff v4.2.0 → v4.2.1 for Netty config changes.

", - createdAt: minutesAgo(60 * 33 + 50), - }, - { - id: "cmt-1015-4", - caseId: "case-1015", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - '

The heap dump shows a Netty buffer pool leak. The full leak suspect from Eclipse MAT:

The thread io.netty.channel.nio.NioEventLoop @ 0x7f8a2c3d4f00 NioEventLoop-3-1\nkeeps local variables with total size 1,432,891,032 (71.46%) bytes.\n\nThe memory is accumulated in one instance of\n"io.netty.buffer.PooledByteBufAllocator$PoolThreadLocalCache" loaded by\n"jdk.internal.loader.ClassLoaders$AppClassLoader @ 0x7f8a2c000028".

And the suspicious thread stack from the dump:

at io.netty.buffer.PoolArena.allocateNormal(PoolArena.java:241)\nat io.netty.buffer.PoolArena.allocate(PoolArena.java:215)\nat io.netty.buffer.PoolArena.allocate(PoolArena.java:147)\nat io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:393)\nat io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:188)\nat io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:179)\nat io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:140)\nat io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:114)\nat io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:147)\nat io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788)\nat io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724)\nat io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650)\nat io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562)\nat io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)\nat java.base/java.lang.Thread.run(Thread.java:840)

This pattern matches a known issue we fixed in v4.2.2: the new Netty connection-keepalive default was tuned aggressively and caused pool buffers to be cached per-thread without an eviction policy.

', - createdAt: minutesAgo(60 * 32 + 10), - }, - { - id: "cmt-1015-5", - caseId: "case-1015", - authorName: "Daniel Owens", - authorRole: "customer", - bodyHtml: - "

That looks like exactly what we're seeing. Is v4.2.2 available? Should we wait for the patch or is there a workaround?

", - createdAt: minutesAgo(60 * 31 + 40), - }, - { - id: "cmt-1015-6", - caseId: "case-1015", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - '

Two options:

(A) Workaround (apply now, no downtime): override the Netty allocator settings via JVM args on the deployment:

env:\n  - name: JAVA_OPTS\n    value: "-Dio.netty.allocator.maxOrder=9 -Dio.netty.allocator.numDirectArenas=4 -Dio.netty.allocator.cacheTrimInterval=4000"

This caps the per-thread direct-buffer cache and trims it every 4s. We\'ve validated this on three customer environments — it brings the steady-state direct memory back to ~280MB.

(B) Patch: v4.2.2 is rolling out next Tuesday (full release). We can backport the fix for you to v4.2.1-hotfix-3 today if you prefer staying on .1 line. Let me know.

', - createdAt: minutesAgo(60 * 31 + 15), - }, - { - id: "cmt-1015-wn-2", - caseId: "case-1015", - authorName: "Lakshmi I.", - authorRole: "wso2_engineer", - internal: true, - bodyHtml: - "

Coordinating with R&D on the hotfix release. Budhi confirmed the cherry-pick from v4.2.2 → v4.2.1-hotfix-3 is straightforward (single commit on the Netty allocator config).

FYI Daniel is on the Acme Choreo account, managed-cloud tier — we should prioritize hotfix path B.

", - createdAt: minutesAgo(60 * 30 + 50), - }, - { - id: "cmt-1015-7", - caseId: "case-1015", - authorName: "Daniel Owens", - authorRole: "customer", - bodyHtml: - "

Applied (A) on the staging deployment first. Direct memory steady at 240–290MB across 4 pods for the last 30 min, no OOMs. Rolling to prod in 15 min unless you say otherwise.

", - createdAt: minutesAgo(60 * 30), - }, - { - id: "cmt-1015-8", - caseId: "case-1015", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Looks good. Go ahead with prod rollout. Keep us posted on the first 2 hours of steady-state metrics.

Re: option B — we'll prep v4.2.1-hotfix-3 regardless; you can take it whenever your change window allows.

", - createdAt: minutesAgo(60 * 29 + 50), - }, - { - id: "cmt-1015-9", - caseId: "case-1015", - authorName: "Daniel Owens", - authorRole: "customer", - bodyHtml: - "

Prod rollout complete. Memory profile looks flat for the last 90 min. The 40-min OOM cycle is gone.

One follow-up: we noticed a slight latency increase (p99 from 18ms → 27ms) since applying the JVM args. Is that expected? If so, we can live with it; just want to confirm.

", - createdAt: minutesAgo(60 * 28), - }, - { - id: "cmt-1015-10", - caseId: "case-1015", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Yes — expected. cacheTrimInterval=4000 trims the per-thread buffer cache every 4s; that adds a small reallocation cost on the next request after each trim. Trade-off is ~5–10ms p99 bump vs ~1.2GB direct memory headroom. Most customers prefer the headroom.

You can tune via cacheTrimInterval if you want a different balance — e.g. 8000 halves the latency penalty but doubles peak retained memory.

", - createdAt: minutesAgo(60 * 27 + 50), - }, - { - id: "cmt-1015-11", - caseId: "case-1015", - authorName: "Daniel Owens", - authorRole: "customer", - bodyHtml: - "

Understood. We'll stick with 4000 for now. Will plan to take v4.2.1-hotfix-3 during next Wednesday's change window.

", - createdAt: minutesAgo(60 * 27 + 40), - }, - { - id: "cmt-1015-wn-3", - caseId: "case-1015", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - internal: true, - bodyHtml: - "

Work note: Linking this case to INC-5012 for the platform-wide tracker. Daniel's account is the third managed-cloud customer hitting this since the v4.2.1 rollout — pattern confirmed. R&D is fast-tracking the proper fix into v4.2.2 GA next week.

Adding @Asanka as watcher since he owns the Choreo runtime SRE on-call this week.

", - createdAt: minutesAgo(60 * 26), - }, - { - id: "cmt-1015-12", - caseId: "case-1015", - authorName: "Asanka R.", - authorRole: "wso2_engineer", - internal: true, - bodyHtml: - "

Thanks for the loop in. Picking up the platform-side rollout coordination — I'll make sure the v4.2.2 release notes call out this fix prominently so customers know to take it.

", - createdAt: minutesAgo(60 * 25 + 40), - }, - { - id: "cmt-1015-13", - caseId: "case-1015", - authorName: "Daniel Owens", - authorRole: "customer", - bodyHtml: - "

Update: 18 hours of steady-state production traffic with the workaround. Zero OOMs, memory profile flat, latency stable at p99=27ms. Calling this resolved on our end.

One question — for the hotfix rollout next Wednesday, do we need to revert the JVM args, or do they coexist safely with the fixed allocator?

", - createdAt: minutesAgo(60 * 9), - }, - { - id: "cmt-1015-14", - caseId: "case-1015", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - bodyHtml: - "

Great to hear. Yes, please revert the JVM args when you apply the hotfix — the new allocator defaults in v4.2.1-hotfix-3 are tuned to be safe out-of-the-box, and leaving the manual override on top will give you back the latency penalty without any benefit.

Suggested rollout:

  1. Apply v4.2.1-hotfix-3 to staging, remove the JAVA_OPTS overrides.
  2. Run for ~2 hours, confirm flat memory profile + p99 back to ~18ms.
  3. Roll to prod with the same change.

Marking this case as solution proposed once you confirm the hotfix path is acceptable.

", - createdAt: minutesAgo(60 * 8 + 40), - }, - { - id: "cmt-1015-15", - caseId: "case-1015", - authorName: "Daniel Owens", - authorRole: "customer", - bodyHtml: - "

Sounds good. Hotfix path acceptable. We'll execute Wednesday change window. Thanks for the thorough investigation and fast turnaround.

", - createdAt: minutesAgo(60 * 7), - }, - ], -}; - -// Seed a few work notes on cases that already have comment threads to make -// the unified Activities feed look realistic out of the box. -const WORK_NOTES: CsmCaseComment[] = [ - { - id: "wn-1001-1", - caseId: "case-1001", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - internal: true, - bodyHtml: - "

Work note: Pulled GC logs from IS node 2. Heap usage normal; suspect connection pool exhaustion on the JDBC userstore.

", - createdAt: minutesAgo(60 * 5), - }, - { - id: "wn-1001-2", - caseId: "case-1001", - authorName: "Lakshmi I.", - authorRole: "wso2_engineer", - internal: true, - bodyHtml: - "

Looped in @Priya for a second look. Will reply to customer once we have a confirmed root cause.

", - createdAt: minutesAgo(60 * 4), - }, - { - id: "wn-1002-1", - caseId: "case-1002", - authorName: "Sajith Ekanayaka", - authorRole: "wso2_engineer", - internal: true, - bodyHtml: - "

Work note: Pod-2 thread dump shows 92 threads stuck in BlockingChannel.read — looks like the same pattern from INC-4877.

", - createdAt: minutesAgo(60 * 1 + 30), - }, -]; -for (const wn of WORK_NOTES) { - const bucket = SEED_COMMENTS[wn.caseId] ?? []; - bucket.push(wn); - SEED_COMMENTS[wn.caseId] = bucket; -} - -const runtime: Map = new Map( - Object.entries(SEED_COMMENTS).map(([k, v]) => [k, [...v]]), -); - -function ensureBucket(caseId: string): CsmCaseComment[] { - let bucket = runtime.get(caseId); - if (!bucket) { - bucket = []; - runtime.set(caseId, bucket); - } - return bucket; -} - -export function getMockCsmCaseComments(caseId: string): CsmCaseComment[] { - // Return a fresh array so callers can't mutate our store. - return [...ensureBucket(caseId)].sort((a, b) => - a.createdAt.localeCompare(b.createdAt), - ); -} - -export interface MockPostCsmCaseCommentInput { - caseId: string; - bodyHtml: string; - authorName: string; - authorRole: "wso2_engineer"; - internal?: boolean; -} - -export function postMockCsmCaseComment( - input: MockPostCsmCaseCommentInput, -): CsmCaseComment { - const comment: CsmCaseComment = { - id: `cmt-${input.caseId}-${Date.now()}`, - caseId: input.caseId, - authorName: input.authorName, - authorRole: input.authorRole, - bodyHtml: input.bodyHtml, - createdAt: new Date().toISOString(), - internal: input.internal ?? false, - }; - ensureBucket(input.caseId).push(comment); - return comment; -} diff --git a/apps/csm-portal/webapp/src/features/csm-cases/api/useCsmCaseAttachments.ts b/apps/csm-portal/webapp/src/features/csm-cases/api/useCsmCaseAttachments.ts index bc76130bc8..338979a53e 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/api/useCsmCaseAttachments.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/api/useCsmCaseAttachments.ts @@ -22,9 +22,8 @@ import { type UseMutationResult, type UseQueryResult, } from "@tanstack/react-query"; -import { useLogger } from "@hooks/useLogger"; import { ApiQueryKeys } from "@constants/apiConstants"; -import { isMockMode, useBackendApi } from "@api/backend/client"; +import { useBackendApi } from "@api/backend/client"; import type { BeAttachmentCreatePayload, BeAttachmentCreateResponse, @@ -32,14 +31,8 @@ import type { BeAttachmentSearchResponse, } from "@api/backend/types"; import { uiAttachmentFromBe } from "@api/backend/mappers"; -import { - getMockCsmCaseAttachments, - postMockCsmCaseAttachment, -} from "@features/csm-cases/api/mocks/casesMocks"; import type { CaseAttachment } from "@features/csm-cases/types/csmCases"; -const MOCK_LATENCY_MS = 150; - /** * Page size used by the attachments list. A single wide page is enough for the * case-detail view. If a case ever exceeds this, switch to an explicit @@ -87,7 +80,6 @@ function saveBlob(blob: Blob, filename: string): void { export function useGetCsmCaseAttachments( caseId: string | undefined, ): UseQueryResult { - const logger = useLogger(); const api = useBackendApi(); return useQuery({ @@ -95,14 +87,6 @@ export function useGetCsmCaseAttachments( queryFn: async (): Promise => { if (!caseId) return []; - if (isMockMode()) { - logger.debug( - `[useGetCsmCaseAttachments] Returning mock attachments for ${caseId}`, - ); - await new Promise((r) => setTimeout(r, MOCK_LATENCY_MS)); - return getMockCsmCaseAttachments(caseId); - } - const payload: BeAttachmentSearchPayload = { pagination: { offset: 0, limit: ATTACHMENTS_PAGE_LIMIT }, }; @@ -141,7 +125,6 @@ export function usePostCsmCaseAttachment(): UseMutationResult< Error, PostCsmCaseAttachmentInput > { - const logger = useLogger(); const api = useBackendApi(); const queryClient = useQueryClient(); @@ -155,20 +138,6 @@ export function usePostCsmCaseAttachment(): UseMutationResult< ); } - if (isMockMode()) { - logger.debug( - `[usePostCsmCaseAttachment] Posting mock attachment for ${input.caseId}`, - ); - await new Promise((r) => setTimeout(r, MOCK_LATENCY_MS)); - return postMockCsmCaseAttachment({ - caseId: input.caseId, - filename: input.name?.trim() || input.file.name, - size: input.file.size, - contentType: input.file.type || "application/octet-stream", - uploadedBy: input.uploadedBy, - }); - } - const dataUri = await readFileAsDataUrl(input.file); const payload: BeAttachmentCreatePayload = { name: input.name?.trim() || input.file.name, @@ -200,23 +169,10 @@ export function useDownloadCsmCaseAttachment(): ( caseId: string, attachment: CaseAttachment, ) => Promise { - const logger = useLogger(); const api = useBackendApi(); return useCallback( async (caseId: string, attachment: CaseAttachment): Promise => { - if (isMockMode()) { - logger.debug( - `[useDownloadCsmCaseAttachment] Mock download of ${attachment.filename}`, - ); - const placeholder = new Blob( - [`Mock content for ${attachment.filename}`], - { type: "text/plain" }, - ); - saveBlob(placeholder, attachment.filename); - return; - } - const blob = await api.getBlob( `/cases/${encodeURIComponent(caseId)}/attachments/${encodeURIComponent( attachment.id, @@ -224,6 +180,6 @@ export function useDownloadCsmCaseAttachment(): ( ); saveBlob(blob, attachment.filename); }, - [api, logger], + [api], ); } diff --git a/apps/csm-portal/webapp/src/features/csm-cases/api/useCsmCaseComments.ts b/apps/csm-portal/webapp/src/features/csm-cases/api/useCsmCaseComments.ts index f0d567870b..c66635cdec 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/api/useCsmCaseComments.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/api/useCsmCaseComments.ts @@ -21,9 +21,8 @@ import { type UseMutationResult, type UseQueryResult, } from "@tanstack/react-query"; -import { useLogger } from "@hooks/useLogger"; import { ApiQueryKeys, BE_MAX_PAGE_LIMIT } from "@constants/apiConstants"; -import { isMockMode, useBackendApi } from "@api/backend/client"; +import { useBackendApi } from "@api/backend/client"; import type { BeCaseComment, BeCaseCommentCreatePayload, @@ -34,14 +33,8 @@ import { commentTypeFromInternal, uiCommentFromBe, } from "@api/backend/mappers"; -import { - getMockCsmCaseComments, - postMockCsmCaseComment, -} from "@features/csm-cases/api/mocks/commentsMocks"; import type { CsmCaseComment } from "@features/csm-cases/types/csmCases"; -const MOCK_LATENCY_MS = 150; - /** Page size used by the comments list. Capped by the BE; see BE_MAX_PAGE_LIMIT. */ const COMMENTS_PAGE_LIMIT = BE_MAX_PAGE_LIMIT; @@ -54,7 +47,6 @@ const COMMENTS_PAGE_LIMIT = BE_MAX_PAGE_LIMIT; export function useGetCsmCaseComments( caseId: string | undefined, ): UseQueryResult { - const logger = useLogger(); const api = useBackendApi(); return useQuery({ @@ -62,14 +54,6 @@ export function useGetCsmCaseComments( queryFn: async (): Promise => { if (!caseId) return []; - if (isMockMode()) { - logger.debug( - `[useGetCsmCaseComments] Returning mock comments for ${caseId}`, - ); - await new Promise((r) => setTimeout(r, MOCK_LATENCY_MS)); - return getMockCsmCaseComments(caseId); - } - const payload: BeCaseCommentSearchPayload = { pagination: { offset: 0, limit: COMMENTS_PAGE_LIMIT }, }; @@ -104,26 +88,11 @@ export function usePostCsmCaseComment(): UseMutationResult< Error, PostCsmCaseCommentInput > { - const logger = useLogger(); const api = useBackendApi(); const queryClient = useQueryClient(); return useMutation({ mutationFn: async (input): Promise => { - if (isMockMode()) { - logger.debug( - `[usePostCsmCaseComment] Posting mock comment for ${input.caseId}`, - ); - await new Promise((r) => setTimeout(r, MOCK_LATENCY_MS)); - return postMockCsmCaseComment({ - caseId: input.caseId, - bodyHtml: input.bodyHtml, - authorName: input.authorName, - authorRole: "wso2_engineer", - internal: input.internal ?? false, - }); - } - const payload: BeCaseCommentCreatePayload = { type: commentTypeFromInternal(input.internal ?? false), // BE stores rich-text HTML; send the editor output as-is. diff --git a/apps/csm-portal/webapp/src/features/csm-cases/api/useFindMyOngoingCases.ts b/apps/csm-portal/webapp/src/features/csm-cases/api/useFindMyOngoingCases.ts index 132c0de77a..3dec2951ce 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/api/useFindMyOngoingCases.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/api/useFindMyOngoingCases.ts @@ -15,13 +15,12 @@ // under the License. import { useCallback } from "react"; -import { isMockMode, useBackendApi } from "@api/backend/client"; +import { useBackendApi } from "@api/backend/client"; import { useIdTokenClaims } from "@hooks/useIdTokenClaims"; import type { BeCaseSearchPayload, BeCaseSearchResponse, } from "@api/backend/types"; -import { getMockCsmCases } from "@features/csm-cases/api/mocks/casesMocks"; /** A case the current user is actively working (in-progress + ongoing). */ export interface MyOngoingCase { @@ -55,21 +54,6 @@ export function useFindMyOngoingCases(): ( return useCallback( async (excludeCaseId: string): Promise => { - if (isMockMode()) { - return getMockCsmCases("all_customers") - .filter( - (c) => - c.id !== excludeCaseId && - c.assigneeIsMe && - c.state === "work_in_progress" && - c.workState === "ongoing", - ) - .map((c) => ({ - id: c.id, - label: c.wso2CaseId || c.caseNumber || c.subject, - })); - } - // Without our email we can't tell which cases are ours; skip the prompt. if (!myEmail) return []; diff --git a/apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCaseDetail.ts b/apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCaseDetail.ts index b0690f76a9..78577c0dae 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCaseDetail.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCaseDetail.ts @@ -15,17 +15,13 @@ // under the License. import { useQuery, type UseQueryResult } from "@tanstack/react-query"; -import { useLogger } from "@hooks/useLogger"; import { useIdTokenClaims } from "@hooks/useIdTokenClaims"; import { ApiQueryKeys } from "@constants/apiConstants"; -import { isMockMode, useBackendApi } from "@api/backend/client"; +import { useBackendApi } from "@api/backend/client"; import { severityFromPriority, uiStateFromBe } from "@api/backend/mappers"; import type { BeCaseView } from "@api/backend/types"; -import { getMockCsmCaseDetailById } from "@features/csm-cases/api/mocks/casesMocks"; import type { CsmCaseDetail } from "@features/csm-cases/types/csmCases"; -const MOCK_LATENCY_MS = 150; - /** * Build a CsmCaseDetail from the rich `BeCaseView`. The view embeds the * account / project / deployment / deployed-product / reporter as objects, so @@ -112,13 +108,11 @@ function detailFromBeCase( * Look up a single CSM case by id. Returns `null` (not an error) when the * id is unknown so the page can render a not-found state for that case. * - * Calls `GET /cases/{id}` in LIVE mode and falls back to the seeded mock - * detail when the mock toggle is on. + * Calls `GET /cases/{id}`. */ export function useGetCsmCaseDetail( caseId: string | undefined, ): UseQueryResult { - const logger = useLogger(); const api = useBackendApi(); // Signed-in user's email, used to resolve `assigneeIsMe` against the case's // assigned-engineer email. In the query key so a late-arriving claim recomputes. @@ -129,12 +123,6 @@ export function useGetCsmCaseDetail( queryFn: async (): Promise => { if (!caseId) return null; - if (isMockMode()) { - logger.debug(`[useGetCsmCaseDetail] Returning mock case ${caseId}`); - await new Promise((r) => setTimeout(r, MOCK_LATENCY_MS)); - return getMockCsmCaseDetailById(caseId) ?? null; - } - const beCase = await api.get( `/cases/${encodeURIComponent(caseId)}`, ); diff --git a/apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCases.ts b/apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCases.ts index 110459c09c..1f4a21d151 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCases.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCases.ts @@ -22,7 +22,7 @@ import { import { useLogger } from "@hooks/useLogger"; import { useIdTokenClaims } from "@hooks/useIdTokenClaims"; import { ApiQueryKeys, BE_MAX_PAGE_LIMIT } from "@constants/apiConstants"; -import { isMockMode, useBackendApi, type BackendApi } from "@api/backend/client"; +import { useBackendApi, type BackendApi } from "@api/backend/client"; import { beStateFromUi, priorityFromSeverity, @@ -37,18 +37,12 @@ import type { BeCaseSearchPayload, BeCaseSearchResponse, } from "@api/backend/types"; -import { getMockCsmCases } from "@features/csm-cases/api/mocks/casesMocks"; import type { CasesFilters } from "@features/csm-cases/components/CasesFilterBar"; -import { - applyCasesFilters, - sortBySlaUrgency, -} from "@features/csm-cases/utils/casesClientFilter"; import type { CsmCaseRow, CsmCasesListResponse, } from "@features/csm-cases/types/csmCases"; -const MOCK_LATENCY_MS = 200; /** Page size for the account name lookup (customer column). */ const LOOKUP_PAGE_LIMIT = BE_MAX_PAGE_LIMIT; // Cap the account scan the same way useProjectOptions caps the project scan. @@ -89,7 +83,7 @@ function accountOptionsQueryOptions(api: BackendApi) { /** * Cross-project CSM cases list. * - * LIVE mode does a single `POST /cases/search` (the flat, cross-project search) + * Does a single `POST /cases/search` (the flat, cross-project search) * and maps each rich `CaseSearchView` — which embeds project / deployment / * deployed-product — to the UI `CsmCaseRow`. The account (customer) name is the * only field not embedded, so it's resolved via `projects/search` @@ -103,9 +97,7 @@ function accountOptionsQueryOptions(api: BackendApi) { * Search and the severity / state / project filters are pushed into the search * payload (searchQuery / severityKeys / stateKeys / projectIds) and the BE * paginates the result (`pagination` → `total` / `limit` / `offset` / - * `hasMore`). The remaining filters (assignee, SLA, product) have no BE support - * and are disabled in LIVE; they only do anything in MOCK mode, where the whole - * seeded dataset is filtered, sorted and sliced client-side here. + * `hasMore`). * * `page` is zero-based (matching MUI `TablePagination`); `pageSize` is the row * limit (≤ {@link BE_MAX_PAGE_LIMIT}). With no filters the backend sorts by @@ -133,40 +125,16 @@ export function useGetCsmCases( // (["S1","S2"] and ["S2","S1"] are the same query). queryKey: [ ApiQueryKeys.CSM_CASES, - filters.scope, search, [...filters.severities].sort(), [...filters.states].sort(), [...filters.projects].sort(), - // MOCK-only filters still affect the sliced result in mock mode. - filters.sla, [...filters.assignees].sort(), - [...filters.products].sort(), currentUserEmail ?? "", page, pageSize, ], queryFn: async (): Promise => { - if (isMockMode()) { - logger.debug( - `[useGetCsmCases] Returning mock cases for scope=${filters.scope}`, - ); - await new Promise((r) => setTimeout(r, MOCK_LATENCY_MS)); - const all = getMockCsmCases(filters.scope); - const matched = applyCasesFilters(all, filters) - .slice() - .sort(sortBySlaUrgency); - const pageRows = matched.slice(offset, offset + pageSize); - return { - scope: filters.scope, - cases: pageRows, - total: matched.length, - limit: pageSize, - offset, - hasMore: offset + pageRows.length < matched.length, - }; - } - // One cross-project case search, plus project/account lookups for the // customer column (cases embed the project, but not its account). The // lookups go through `fetchQuery` with their own stable keys, so they @@ -254,7 +222,6 @@ export function useGetCsmCases( }); return { - scope: filters.scope, cases, total: casesResponse.total ?? cases.length, limit: casesResponse.limit ?? pageSize, diff --git a/apps/csm-portal/webapp/src/features/csm-cases/api/usePatchCsmCase.ts b/apps/csm-portal/webapp/src/features/csm-cases/api/usePatchCsmCase.ts index e348fd3194..a7188ceacd 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/api/usePatchCsmCase.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/api/usePatchCsmCase.ts @@ -21,14 +21,12 @@ import { } from "@tanstack/react-query"; import { useCallback } from "react"; import { ApiQueryKeys } from "@constants/apiConstants"; -import { isMockMode, useBackendApi } from "@api/backend/client"; +import { useBackendApi } from "@api/backend/client"; import type { BeCaseUpdatePayload, BeUpdateCaseResponse, } from "@api/backend/types"; -const MOCK_LATENCY_MS = 200; - /** * Update a case via `PATCH /cases/{id}` — state transitions, priority changes, * work sub-state (`workState`), assignee (`assigneeEmail`), or watch list @@ -37,9 +35,6 @@ const MOCK_LATENCY_MS = 200; * The response is `BeUpdateCaseResponse` ({ message, case }), but on success we * ignore the body and invalidate this case's detail query and the cross-project * list so both refetch the authoritative state (incl. fresh `nextStates`). - * - * In MOCK mode the mutation resolves without persisting (mock detail data is - * static), so the caller's optimistic feedback is the only visible effect. */ export function usePatchCsmCase( caseId: string | undefined, @@ -52,13 +47,6 @@ export function usePatchCsmCase( if (!caseId) { throw new Error("Cannot update a case without an id."); } - if (isMockMode()) { - await new Promise((r) => setTimeout(r, MOCK_LATENCY_MS)); - // The success handler ignores the body and refetches, so the mock only - // needs the id. (Echoing input.state/priority would not type-check - // against the single-field union and isn't used anyway.) - return { message: "Case updated (mock).", case: { id: caseId } }; - } return api.patch( `/cases/${encodeURIComponent(caseId)}`, input, @@ -90,14 +78,10 @@ export function usePatchCsmCaseById(): ( return useCallback( async (caseId: string, input: BeCaseUpdatePayload): Promise => { if (!caseId) throw new Error("Cannot update a case without an id."); - if (isMockMode()) { - await new Promise((r) => setTimeout(r, MOCK_LATENCY_MS)); - } else { - await api.patch( - `/cases/${encodeURIComponent(caseId)}`, - input, - ); - } + await api.patch( + `/cases/${encodeURIComponent(caseId)}`, + input, + ); queryClient.invalidateQueries({ queryKey: [ApiQueryKeys.CSM_CASE_DETAIL, caseId], }); diff --git a/apps/csm-portal/webapp/src/features/csm-cases/api/usePostCsmCase.ts b/apps/csm-portal/webapp/src/features/csm-cases/api/usePostCsmCase.ts index 91e7a2207b..baedbe0099 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/api/usePostCsmCase.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/api/usePostCsmCase.ts @@ -20,23 +20,18 @@ import { type UseMutationResult, } from "@tanstack/react-query"; import { ApiQueryKeys } from "@constants/apiConstants"; -import { isMockMode, useBackendApi } from "@api/backend/client"; +import { useBackendApi } from "@api/backend/client"; import type { BeCaseCreatePayload, BeCaseCreateResponse, BeCreatedCase, } from "@api/backend/types"; -const MOCK_LATENCY_MS = 200; - /** * Create a case via `POST /cases`. The backend wraps the result in a * `{ message, case }` envelope, so this unwraps and returns the created case * (its `id` drives the post-create redirect). The mutation also invalidates * project-scoped case searches so list views refresh. - * - * In MOCK mode the mutation resolves with a fabricated case using the input - * payload; nothing is persisted server-side. */ export function usePostCsmCase(): UseMutationResult< BeCreatedCase, @@ -48,17 +43,6 @@ export function usePostCsmCase(): UseMutationResult< return useMutation({ mutationFn: async (input): Promise => { - if (isMockMode()) { - await new Promise((r) => setTimeout(r, MOCK_LATENCY_MS)); - const now = new Date().toISOString(); - return { - id: `mock-${Math.random().toString(36).slice(2, 10)}`, - number: `CS-MOCK-${Math.floor(Math.random() * 9000) + 1000}`, - createdBy: "mock@wso2.com", - createdOn: now, - state: "Open", - }; - } const res = await api.post( "/cases", input, diff --git a/apps/csm-portal/webapp/src/features/csm-cases/api/useQuickCaseSearch.ts b/apps/csm-portal/webapp/src/features/csm-cases/api/useQuickCaseSearch.ts index f3638a9528..338062eeef 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/api/useQuickCaseSearch.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/api/useQuickCaseSearch.ts @@ -15,17 +15,12 @@ // under the License. import { useQuery, type UseQueryResult } from "@tanstack/react-query"; -import { useLogger } from "@hooks/useLogger"; import { ApiQueryKeys } from "@constants/apiConstants"; -import { isMockMode, useBackendApi } from "@api/backend/client"; +import { useBackendApi } from "@api/backend/client"; import type { BeCaseSearchPayload, BeCaseSearchResponse, } from "@api/backend/types"; -import { getMockCsmCases } from "@features/csm-cases/api/mocks/casesMocks"; -import { caseIdLabel } from "@features/csm-cases/utils/caseIdentity"; - -const MOCK_LATENCY_MS = 120; /** Don't fire a search until the user has typed something searchable. */ export const QUICK_CASE_MIN_QUERY_LEN = 2; @@ -52,39 +47,17 @@ export interface QuickCaseHit { * matching cases the user can jump straight into. * * The query is disabled until the trimmed text reaches - * {@link QUICK_CASE_MIN_QUERY_LEN}, so opening the palette costs no network. In - * MOCK mode it filters the seeded set client-side on id/subject. + * {@link QUICK_CASE_MIN_QUERY_LEN}, so opening the palette costs no network. */ export function useQuickCaseSearch( query: string, ): UseQueryResult { - const logger = useLogger(); const api = useBackendApi(); const q = query.trim(); return useQuery({ queryKey: [ApiQueryKeys.CSM_CASES, "quick-search", q], queryFn: async (): Promise => { - if (isMockMode()) { - logger.debug(`[useQuickCaseSearch] mock case search for "${q}"`); - await new Promise((r) => setTimeout(r, MOCK_LATENCY_MS)); - const needle = q.toLowerCase(); - return getMockCsmCases("all_customers") - .filter((c) => { - const id = caseIdLabel(c).toLowerCase(); - return ( - id.includes(needle) || c.subject.toLowerCase().includes(needle) - ); - }) - .slice(0, QUICK_CASE_LIMIT) - .map((c) => ({ - id: c.id, - caseNumber: c.caseNumber, - wso2CaseId: c.wso2CaseId, - subject: c.subject, - })); - } - const res = await api.post( "/cases/search", { diff --git a/apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.test.tsx b/apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.test.tsx index 107a11087f..cafff83432 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.test.tsx +++ b/apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.test.tsx @@ -18,21 +18,65 @@ import { fireEvent, render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import "@testing-library/jest-dom/vitest"; import CaseActionBar from "@features/csm-cases/components/CaseActionBar"; -import { getMockCsmCaseDetailById } from "@features/csm-cases/api/mocks/casesMocks"; import type { CsmCaseDetail } from "@features/csm-cases/types/csmCases"; import type { CaseState } from "@features/csm-dashboard/types/abtDashboard"; +/** A complete, minimal case-detail fixture; tests override state/nextStates. */ +const BASE_CASE: CsmCaseDetail = { + id: "case-1001", + caseNumber: "CS-1001", + wso2CaseId: "ACMESUB-1001", + subject: "Identity Server token issuance latency spike", + customer: "Acme Financial", + accountId: "acc-001", + projectId: "prj-acme-iam-prod", + projectName: "IAM Production", + product: "WSO2 Identity Server", + severity: "S1", + state: "work_in_progress", + workState: "ongoing", + assignee: "Jane Doe", + assigneeIsMe: true, + slaClockType: "first_response", + minutesToBreach: 120, + hasSla: false, + createdAt: "2026-01-01T00:00:00.000Z", + updatedAt: "2026-01-01T01:00:00.000Z", + description: "Token issuance latency has spiked.", + assignmentGroup: "grp.cre_team", + customerContext: { + accountName: "Acme Financial", + tier: "enterprise", + region: "us-east-1", + primaryContact: "Jane Doe", + primaryContactEmail: "jane.doe@example.com", + accountManager: "John Roe", + openCases: 1, + }, + productContext: { + product: "WSO2 Identity Server", + version: "7.1.0", + deployment: "IAM Production", + environment: "prod", + }, + slaClocks: [], + watchers: [], + linkedItems: [], + tags: [], + timeLogs: [], + audit: [], + attachments: [], + isWatching: false, +}; + /** - * Build a case-detail fixture in a given state with an explicit `nextStates`, - * reusing the seeded mock so every other required field is realistic. + * Build a case-detail fixture in a given state with an explicit `nextStates`. */ function caseInState( state: CaseState, nextStates: CaseState[] | undefined, ): CsmCaseDetail { - const base = getMockCsmCaseDetailById("case-1001"); - if (!base) throw new Error("mock case-1001 missing"); - return { ...base, state, nextStates }; + return { ...BASE_CASE, state, nextStates }; } describe("CaseActionBar — nextStates-driven buttons", () => { diff --git a/apps/csm-portal/webapp/src/features/csm-cases/components/CasesFilterBar.tsx b/apps/csm-portal/webapp/src/features/csm-cases/components/CasesFilterBar.tsx index ab04bba466..e886888e93 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/components/CasesFilterBar.tsx +++ b/apps/csm-portal/webapp/src/features/csm-cases/components/CasesFilterBar.tsx @@ -56,7 +56,6 @@ import { import { useMemo, useState, type JSX } from "react"; import type { CaseState, - DashboardScope, Severity, } from "@features/csm-dashboard/types/abtDashboard"; import { STATE_LABEL } from "@features/csm-dashboard/utils/abtDashboard"; @@ -71,11 +70,8 @@ import { SUGGESTED_FILTER_VIEWS, useSavedFilterViews, } from "@features/csm-cases/utils/savedFilterViews"; -import { isMockMode } from "@api/backend/client"; import AsyncProjectMultiSelect from "@features/csm-cases/components/AsyncProjectMultiSelect"; -export type SlaFilter = "any" | "breached" | "at_risk"; - /** Sentinel used inside `assignees` to mean "the current user". */ export const ASSIGNEE_ME_TOKEN = "@me"; /** Literal assignee name used in mock data for unassigned cases. */ @@ -83,21 +79,18 @@ export const ASSIGNEE_UNASSIGNED = "Unassigned"; /** * Filter state for the CSM cases list. `severities` / `states` are multi-select - * arrays driven by fixed enums. `assignees` / `projects` / `products` are - * free-form multi-selects driven by an Autocomplete with type-to-filter: - * options are derived from the data currently in scope. The `assignees` list - * accepts the sentinel `@me` (resolves against `assigneeIsMe`) and the literal + * arrays driven by fixed enums. `assignees` / `projects` are free-form + * multi-selects driven by an Autocomplete with type-to-filter: options are + * derived from the data currently in scope. The `assignees` list accepts the + * sentinel `@me` (resolves against `assigneeIsMe`) and the literal * "Unassigned"; any other entries are matched against `case.assignee` by name. */ export interface CasesFilters { - scope: DashboardScope; search: string; severities: Severity[]; states: CaseState[]; - sla: SlaFilter; assignees: string[]; projects: string[]; - products: string[]; } /** @@ -120,8 +113,6 @@ interface CasesFilterBarProps { availableAssigneeUsers: AssigneeUser[]; /** Projects for the (id-based) project filter — value is the id, label the name. */ availableProjects: { id: string; name: string }[]; - /** Product names seen in the current data. */ - availableProducts: string[]; } const ALL_SEVERITIES: Severity[] = ["S0", "S1", "S2", "S3", "S4"]; @@ -133,13 +124,6 @@ const PRIMARY_STATES: CaseState[] = [ "waiting_on_wso2", "closed", ]; -const SLA_OPTIONS: { value: SlaFilter; label: string }[] = [ - { value: "any", label: "Any SLA" }, - { value: "at_risk", label: "At risk" }, - { value: "breached", label: "Breached" }, -]; - - /** Pretty-print an assignee value (handle the @me sentinel). */ function assigneeLabel(value: string): string { return value === ASSIGNEE_ME_TOKEN ? "Me" : value; @@ -219,43 +203,6 @@ function MultiSelectField({ ); } -interface SingleSelectFieldProps { - id: string; - label: string; - value: T; - options: { value: T; label: string }[]; - onChange: (next: T) => void; - disabled?: boolean; -} - -function SingleSelectField({ - id, - label, - value, - options, - onChange, - disabled, -}: SingleSelectFieldProps): JSX.Element { - return ( - - {label} - - - ); -} - interface SearchableMultiSelectProps { id: string; label: string; @@ -365,7 +312,6 @@ export default function CasesFilterBar({ onFiltersToggle, availableAssigneeUsers, availableProjects, - availableProducts, }: CasesFilterBarProps): JSX.Element { const activeCount = countActiveFilters(filters); const hasActive = activeCount > 0; @@ -403,19 +349,6 @@ export default function CasesFilterBar({ setSavedAnchor(null); }; - // Scope (ABT), assignee, and SLA have no backend support yet (no assignee or - // SLA fields), so disable them against the live backend — they only do - // anything against seeded mock data. - const beUnsupported = !isMockMode(); - const beUnsupportedReason = - "Not available against the live backend yet (no assignee / SLA data)."; - // Product filtering would need the backend to filter by deployed-product - // instance ids (the case carries an instance id, not the product name), and - // there is no directory to map product name → instance ids. So it is - // disabled in LIVE and only refines the seeded mock data. - const productFilterUnsupportedReason = - "Filtering by product isn't supported by the live backend yet."; - const severityOptions = useMemo( () => ALL_SEVERITIES.map((s) => ({ value: s, label: s })), [], @@ -467,34 +400,8 @@ export default function CasesFilterBar({ return ( - {/* Scope + search + filters toggle. Scope buttons are CSM-specific - (My ABT vs All customers) and live alongside search at the top. */} + {/* Search + saved views + filters toggle. */} - - - - - - - {/* Collapsible filter grid. Severity / state stay as fixed multi-selects; - assignee / project / product are type-to-search Autocompletes. */} + assignee / project are type-to-search Autocompletes. */} {isFiltersOpen && ( <> @@ -688,25 +595,11 @@ export default function CasesFilterBar({ onChange={(next) => onChange({ ...filters, states: next })} /> - - - - onChange({ ...filters, sla: next })} - disabled={beUnsupported} - /> - - - {/* Static option list for now. Once the backend supports assignee search/filtering, switch this to a type-ahead like AsyncProjectMultiSelect rather than loading every user. */} - + onChange({ ...filters, assignees: next }) } - disabled={beUnsupported} + disabled /> @@ -732,25 +625,6 @@ export default function CasesFilterBar({ nameSeed={projectNameSeed} /> - - - - - onChange({ ...filters, products: next }) - } - disabled={beUnsupported} - /> - - - {activeCount > 0 && ( diff --git a/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx b/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx index 64f97c8191..a81b835795 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx +++ b/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx @@ -719,7 +719,7 @@ export default function CsmCaseDetailPage(): JSX.Element { Case not found - No case with id {caseId} in the current mock dataset. + No case found with id {caseId}. ); diff --git a/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCasesPage.tsx b/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCasesPage.tsx index 1cddc838f2..bbfdf001dd 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCasesPage.tsx +++ b/apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCasesPage.tsx @@ -106,7 +106,7 @@ export default function CsmCasesPage(): JSX.Element { if (!isError) hasShownErrorRef.current = false; }, [isError, error, showError]); - // The backend (or the mock hook) already filtered, sorted and paged the rows; + // The backend already filtered, sorted and paged the rows; // render exactly what came back for the current page. const cases = data?.cases ?? []; @@ -134,14 +134,6 @@ export default function CsmCasesPage(): JSX.Element { return Array.from(byId, ([id, name]) => ({ id, name })); }, [data?.cases]); - const availableProducts = useMemo(() => { - const set = new Set(); - (data?.cases ?? []).forEach((c) => { - if (c.product) set.add(c.product); - }); - return Array.from(set).sort(); - }, [data?.cases]); - const total = data?.total ?? 0; // If the total shrinks (a background refetch, or rows closing) while we're on // a later page, clamp back to the last valid page so the table never shows an @@ -216,14 +208,11 @@ export default function CsmCasesPage(): JSX.Element { - setFilters({ ...DEFAULT_CASES_FILTERS, scope: filters.scope }) - } + onReset={() => setFilters(DEFAULT_CASES_FILTERS)} isFiltersOpen={isFiltersOpen} onFiltersToggle={() => setIsFiltersOpen((v) => !v)} availableAssigneeUsers={availableAssigneeUsers} availableProjects={availableProjects} - availableProducts={availableProducts} /> diff --git a/apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts b/apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts index 6eebae18cd..b78ff4eb86 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts @@ -17,7 +17,6 @@ import type { CaseState, CaseWorkState, - DashboardScope, Severity, SlaClockType, } from "@features/csm-dashboard/types/abtDashboard"; @@ -74,7 +73,6 @@ export interface CsmCaseRow { } export interface CsmCasesListResponse { - scope: DashboardScope; cases: CsmCaseRow[]; /** Total rows matching the query across all pages (BE `total`). */ total: number; diff --git a/apps/csm-portal/webapp/src/features/csm-cases/utils/casesClientFilter.ts b/apps/csm-portal/webapp/src/features/csm-cases/utils/casesClientFilter.ts deleted file mode 100644 index a802348a78..0000000000 --- a/apps/csm-portal/webapp/src/features/csm-cases/utils/casesClientFilter.ts +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -import { ASSIGNEE_ME_TOKEN } from "@features/csm-cases/components/CasesFilterBar"; -import type { CasesFilters } from "@features/csm-cases/components/CasesFilterBar"; -import type { CsmCaseRow } from "@features/csm-cases/types/csmCases"; - -export const SLA_AT_RISK_THRESHOLD_MINUTES = 60; - -/** - * Apply the full filter set to a case list client-side. Used for MOCK mode, - * where the hook holds the whole seeded dataset and must filter + paginate it - * itself. In LIVE mode the backend applies the supported filters - * (search / severity / state / project) server-side, so this is not used. - */ -export function applyCasesFilters( - cases: CsmCaseRow[], - f: CasesFilters, -): CsmCaseRow[] { - const q = f.search.trim().toLowerCase(); - return cases.filter((c) => { - if (f.severities.length && !f.severities.includes(c.severity)) return false; - if (f.states.length && !f.states.includes(c.state)) return false; - // SLA status only applies to active cases; a closed row (often - // minutesToBreach = 0) must not match "breached" or "at_risk". - if (f.sla === "breached") { - if (c.state === "closed" || c.minutesToBreach >= 0) return false; - } - if ( - f.sla === "at_risk" && - !( - c.state !== "closed" && - c.minutesToBreach >= 0 && - c.minutesToBreach <= SLA_AT_RISK_THRESHOLD_MINUTES - ) - ) - return false; - if (f.assignees.length) { - const match = f.assignees.some((a) => - a === ASSIGNEE_ME_TOKEN ? c.assigneeIsMe : a === c.assignee, - ); - if (!match) return false; - } - if (f.projects.length && !f.projects.includes(c.projectId)) return false; - if (f.products.length && !f.products.includes(c.product)) return false; - if (q) { - const hay = - `${c.caseNumber ?? ""} ${c.wso2CaseId ?? ""} ${c.subject} ${c.customer} ${c.projectName} ${c.assignee} ${c.product}`.toLowerCase(); - if (!hay.includes(q)) return false; - } - return true; - }); -} - -/** Sort by SLA urgency: closed last, then soonest-to-breach first. */ -export function sortBySlaUrgency(a: CsmCaseRow, b: CsmCaseRow): number { - const aClosed = a.state === "closed" ? 1 : 0; - const bClosed = b.state === "closed" ? 1 : 0; - if (aClosed !== bClosed) return aClosed - bClosed; - return a.minutesToBreach - b.minutesToBreach; -} diff --git a/apps/csm-portal/webapp/src/features/csm-cases/utils/casesFiltersUrl.test.ts b/apps/csm-portal/webapp/src/features/csm-cases/utils/casesFiltersUrl.test.ts index 00732ddfca..70d16f05d0 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/utils/casesFiltersUrl.test.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/utils/casesFiltersUrl.test.ts @@ -32,29 +32,24 @@ describe("readCasesFiltersFromUrl", () => { it("parses a fully-populated query string", () => { const params = new URLSearchParams( - "scope=all_customers&q=timeout&severities=S0,S2&states=open,closed&sla=at_risk&assignees=alice,bob&projects=apim&products=mi", + "q=timeout&severities=S0,S2&states=open,closed&assignees=alice,bob&projects=apim", ); expect(readCasesFiltersFromUrl(params)).toEqual({ - scope: "all_customers", search: "timeout", severities: ["S0", "S2"], states: ["open", "closed"], - sla: "at_risk", assignees: ["alice", "bob"], projects: ["apim"], - products: ["mi"], }); }); it("drops values outside the allowed enums", () => { const params = new URLSearchParams( - "scope=bogus&severities=S0,S9,wat&states=open,nonsense&sla=invalid", + "severities=S0,S9,wat&states=open,nonsense", ); const f = readCasesFiltersFromUrl(params); - expect(f.scope).toBe("my_abt"); // fallback expect(f.severities).toEqual(["S0"]); expect(f.states).toEqual(["open"]); - expect(f.sla).toBe("any"); // fallback }); it("strips empties and over-long free-form entries", () => { @@ -72,14 +67,11 @@ describe("writeCasesFiltersToUrl", () => { it("round-trips a non-default filter set", () => { const filters: CasesFilters = { - scope: "all_customers", search: "disk full", severities: ["S1"], states: ["work_in_progress"], - sla: "breached", assignees: ["carol"], projects: ["streaming"], - products: ["si"], }; const round = readCasesFiltersFromUrl(writeCasesFiltersToUrl(filters)); expect(round).toEqual(filters); @@ -92,8 +84,6 @@ describe("casesHref", () => { }); it("builds a query string from a partial override", () => { - expect(casesHref({ scope: "all_customers" })).toBe( - "/cases?scope=all_customers", - ); + expect(casesHref({ severities: ["S1"] })).toBe("/cases?severities=S1"); }); }); diff --git a/apps/csm-portal/webapp/src/features/csm-cases/utils/casesFiltersUrl.ts b/apps/csm-portal/webapp/src/features/csm-cases/utils/casesFiltersUrl.ts index 3cd2474f03..681d59dac6 100644 --- a/apps/csm-portal/webapp/src/features/csm-cases/utils/casesFiltersUrl.ts +++ b/apps/csm-portal/webapp/src/features/csm-cases/utils/casesFiltersUrl.ts @@ -16,26 +16,18 @@ import type { CaseState, - DashboardScope, Severity, } from "@features/csm-dashboard/types/abtDashboard"; -import type { - CasesFilters, - SlaFilter, -} from "@features/csm-cases/components/CasesFilterBar"; +import type { CasesFilters } from "@features/csm-cases/components/CasesFilterBar"; export const DEFAULT_CASES_FILTERS: CasesFilters = { - scope: "my_abt", search: "", severities: [], states: [], - sla: "any", assignees: [], projects: [], - products: [], }; -const VALID_SCOPES: DashboardScope[] = ["my_abt", "all_customers"]; const VALID_SEVERITIES: Severity[] = ["S0", "S1", "S2", "S3", "S4"]; const VALID_STATES: CaseState[] = [ "open", @@ -45,7 +37,6 @@ const VALID_STATES: CaseState[] = [ "waiting_on_wso2", "closed", ]; -const VALID_SLA: SlaFilter[] = ["any", "at_risk", "breached"]; function parseCsv(raw: string | null, allowed: T[]): T[] { if (!raw) return []; @@ -68,26 +59,15 @@ function parseFreeFormCsv(raw: string | null, maxEntryLen = 120): string[] { .filter((s) => s.length > 0 && s.length <= maxEntryLen); } -function parseEnum( - raw: string | null, - allowed: T[], - fallback: T, -): T { - return raw && (allowed as string[]).includes(raw) ? (raw as T) : fallback; -} - export function readCasesFiltersFromUrl( params: URLSearchParams, ): CasesFilters { return { - scope: parseEnum(params.get("scope"), VALID_SCOPES, "my_abt"), search: params.get("q") ?? "", severities: parseCsv(params.get("severities"), VALID_SEVERITIES), states: parseCsv(params.get("states"), VALID_STATES), - sla: parseEnum(params.get("sla"), VALID_SLA, "any"), assignees: parseFreeFormCsv(params.get("assignees")), projects: parseFreeFormCsv(params.get("projects")), - products: parseFreeFormCsv(params.get("products")), }; } @@ -97,14 +77,11 @@ export function readCasesFiltersFromUrl( */ export function writeCasesFiltersToUrl(f: CasesFilters): URLSearchParams { const out = new URLSearchParams(); - if (f.scope !== "my_abt") out.set("scope", f.scope); if (f.search) out.set("q", f.search); if (f.severities.length) out.set("severities", f.severities.join(",")); if (f.states.length) out.set("states", f.states.join(",")); - if (f.sla !== "any") out.set("sla", f.sla); if (f.assignees.length) out.set("assignees", f.assignees.join(",")); if (f.projects.length) out.set("projects", f.projects.join(",")); - if (f.products.length) out.set("products", f.products.join(",")); return out; } @@ -119,10 +96,8 @@ export function countActiveFilters(f: CasesFilters): number { if (f.search.trim()) n += 1; if (f.severities.length) n += 1; if (f.states.length) n += 1; - if (f.sla !== "any") n += 1; if (f.assignees.length) n += 1; if (f.projects.length) n += 1; - if (f.products.length) n += 1; return n; } diff --git a/apps/csm-portal/webapp/src/features/csm-dashboard/api/mocks/dashboardMocks.ts b/apps/csm-portal/webapp/src/features/csm-dashboard/api/mocks/dashboardMocks.ts deleted file mode 100644 index 8514bdc8d4..0000000000 --- a/apps/csm-portal/webapp/src/features/csm-dashboard/api/mocks/dashboardMocks.ts +++ /dev/null @@ -1,336 +0,0 @@ -// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -import type { - CsmAbtDashboardData, - CsmCustomerSummary, - CsmQueueCase, - CsmRecentActivity, - CsmSlaAtRiskCase, - DashboardScope, -} from "@features/csm-dashboard/types/abtDashboard"; - -const minutesAgo = (n: number): string => - new Date(Date.now() - n * 60_000).toISOString(); - -const ABT_CUSTOMERS: CsmCustomerSummary[] = [ - { - accountId: "acc-001", - accountName: "Acme Financial", - tier: "Platinum", - openCaseCount: 14, - s0s1Count: 2, - breachedCount: 1, - lastActivityAt: minutesAgo(18), - }, - { - accountId: "acc-002", - accountName: "Globex Corp", - tier: "Gold", - openCaseCount: 7, - s0s1Count: 0, - breachedCount: 0, - lastActivityAt: minutesAgo(95), - }, - { - accountId: "acc-003", - accountName: "Initech Systems", - tier: "Platinum", - openCaseCount: 21, - s0s1Count: 3, - breachedCount: 2, - lastActivityAt: minutesAgo(7), - }, - { - accountId: "acc-004", - accountName: "Soylent Industries", - tier: "Silver", - openCaseCount: 4, - s0s1Count: 0, - breachedCount: 0, - lastActivityAt: minutesAgo(240), - }, - { - accountId: "acc-005", - accountName: "Umbrella Health", - tier: "Gold", - openCaseCount: 9, - s0s1Count: 1, - breachedCount: 0, - lastActivityAt: minutesAgo(52), - }, -]; - -const ALL_EXTRA_CUSTOMERS: CsmCustomerSummary[] = [ - { - accountId: "acc-101", - accountName: "Wayne Enterprises", - tier: "Platinum", - openCaseCount: 11, - s0s1Count: 1, - breachedCount: 0, - lastActivityAt: minutesAgo(33), - }, - { - accountId: "acc-102", - accountName: "Stark Industries", - tier: "Gold", - openCaseCount: 6, - s0s1Count: 0, - breachedCount: 0, - lastActivityAt: minutesAgo(180), - }, - { - accountId: "acc-103", - accountName: "Tyrell Corp", - tier: "Silver", - openCaseCount: 3, - s0s1Count: 0, - breachedCount: 0, - lastActivityAt: minutesAgo(420), - }, -]; - -const QUEUE_TOP_CASES: CsmQueueCase[] = [ - { - id: "case-1001", - caseNumber: "CS-1001", - subject: "Identity Server token issuance latency spike", - customer: "Acme Financial", - severity: "S1", - state: "work_in_progress", - slaClockType: "first_response", - minutesToBreach: -22, - }, - { - id: "case-1002", - caseNumber: "CS-1002", - subject: "API Manager gateway 502 during peak", - customer: "Initech Systems", - severity: "S0", - state: "work_in_progress", - slaClockType: "resolution", - minutesToBreach: 12, - }, - { - id: "case-1003", - caseNumber: "CS-1003", - subject: "MI cluster failed to start after upgrade", - customer: "Initech Systems", - severity: "S1", - state: "awaiting_info", - slaClockType: "ack", - minutesToBreach: 47, - }, - { - id: "case-1004", - caseNumber: "CS-1004", - subject: "OIDC userinfo claims missing groups", - customer: "Acme Financial", - severity: "S2", - state: "open", - slaClockType: "ack", - minutesToBreach: 90, - }, - { - id: "case-1005", - caseNumber: "CS-1005", - subject: "Choreo deployment stuck in 'Provisioning'", - customer: "Umbrella Health", - severity: "S2", - state: "work_in_progress", - slaClockType: "first_response", - minutesToBreach: 145, - }, -]; - -const SLA_AT_RISK_ABT: CsmSlaAtRiskCase[] = [ - { - id: "case-1001", - caseNumber: "CS-1001", - subject: "Identity Server token issuance latency spike", - customer: "Acme Financial", - severity: "S1", - assignee: "Me", - slaClockType: "first_response", - minutesToBreach: -22, - state: "work_in_progress", - }, - { - id: "case-1002", - caseNumber: "CS-1002", - subject: "API Manager gateway 502 during peak", - customer: "Initech Systems", - severity: "S0", - assignee: "Me", - slaClockType: "resolution", - minutesToBreach: 12, - state: "work_in_progress", - }, - { - id: "case-1007", - caseNumber: "CS-1007", - subject: "Streaming Integrator backpressure", - customer: "Initech Systems", - severity: "S1", - assignee: "Priya N.", - slaClockType: "resolution", - minutesToBreach: -180, - state: "awaiting_info", - }, - { - id: "case-1008", - caseNumber: "CS-1008", - subject: "Asgardeo SCIM bulk import failing", - customer: "Umbrella Health", - severity: "S2", - assignee: "Dilan W.", - slaClockType: "first_response", - minutesToBreach: 25, - state: "work_in_progress", - }, -]; - -const SLA_AT_RISK_ALL_EXTRA: CsmSlaAtRiskCase[] = [ - { - id: "case-2001", - caseNumber: "CS-2001", - subject: "Open Banking sandbox cert mismatch", - customer: "Wayne Enterprises", - severity: "S1", - assignee: "Tharindu A.", - slaClockType: "first_response", - minutesToBreach: -60, - state: "work_in_progress", - }, - { - id: "case-2002", - caseNumber: "CS-2002", - subject: "Choreo build hanging on private dep", - customer: "Stark Industries", - severity: "S2", - assignee: "Maya R.", - slaClockType: "ack", - minutesToBreach: 75, - state: "open", - }, -]; - -const RECENT_ACTIVITY_ABT: CsmRecentActivity[] = [ - { - id: "act-1", - caseId: "case-1002", - caseNumber: "CS-1002", - customer: "Initech Systems", - type: "comment", - who: "Customer (J. Doe)", - whenAt: minutesAgo(4), - summary: "Provided thread dump and gateway logs from peak window.", - }, - { - id: "act-2", - caseId: "case-1001", - caseNumber: "CS-1001", - customer: "Acme Financial", - type: "sla_breach", - who: "System", - whenAt: minutesAgo(22), - summary: "First-response SLA breached.", - }, - { - id: "act-3", - caseId: "case-1003", - caseNumber: "CS-1003", - customer: "Initech Systems", - type: "state_change", - who: "Me", - whenAt: minutesAgo(48), - summary: "Moved to awaiting_info — requested replication steps.", - }, - { - id: "act-4", - caseId: "case-1004", - caseNumber: "CS-1004", - customer: "Acme Financial", - type: "case_created", - who: "Customer (R. Patel)", - whenAt: minutesAgo(75), - summary: "New case opened: OIDC userinfo claims missing groups.", - }, - { - id: "act-5", - caseId: "case-1005", - caseNumber: "CS-1005", - customer: "Umbrella Health", - type: "comment", - who: "Me", - whenAt: minutesAgo(115), - summary: "Asked customer for kubectl describe output on stuck pod.", - }, -]; - -const ENGINEER = { - name: "Sajith Ekanayaka", - email: "sajithe@wso2.com", - abtName: "ABT-IAM-East", -}; - -const sumQueue = (cases: CsmQueueCase[]) => { - let actionRequired = 0; - let inProgress = 0; - let awaitingInfo = 0; - for (const c of cases) { - if (c.state === "open") actionRequired += 1; - else if (c.state === "work_in_progress") inProgress += 1; - else if (c.state === "awaiting_info") awaitingInfo += 1; - } - return { actionRequired, inProgress, awaitingInfo }; -}; - -export function getMockAbtDashboard( - scope: DashboardScope, -): CsmAbtDashboardData { - const customers = - scope === "my_abt" - ? ABT_CUSTOMERS - : [...ABT_CUSTOMERS, ...ALL_EXTRA_CUSTOMERS]; - - const slaAtRisk = - scope === "my_abt" - ? SLA_AT_RISK_ABT - : [...SLA_AT_RISK_ABT, ...SLA_AT_RISK_ALL_EXTRA]; - - // My Queue is always "me", independent of scope. - const { actionRequired, inProgress, awaitingInfo } = sumQueue(QUEUE_TOP_CASES); - // Count every non-closed case, not just the three display buckets, so the - // total doesn't drop states like solution_proposed / waiting_on_wso2. - const totalOpen = QUEUE_TOP_CASES.filter((c) => c.state !== "closed").length; - - return { - engineer: ENGINEER, - scope, - queue: { - actionRequiredCount: actionRequired, - inProgressCount: inProgress, - awaitingInfoCount: awaitingInfo, - totalOpenCount: totalOpen, - topCases: QUEUE_TOP_CASES, - }, - slaAtRisk, - customers, - recentActivity: RECENT_ACTIVITY_ABT, - }; -} diff --git a/apps/csm-portal/webapp/src/features/csm-dashboard/api/useCaseComposition.ts b/apps/csm-portal/webapp/src/features/csm-dashboard/api/useCaseComposition.ts index 8eea9aea32..6fbaad106c 100644 --- a/apps/csm-portal/webapp/src/features/csm-dashboard/api/useCaseComposition.ts +++ b/apps/csm-portal/webapp/src/features/csm-dashboard/api/useCaseComposition.ts @@ -16,13 +16,12 @@ import { useQuery, type UseQueryResult } from "@tanstack/react-query"; import { ApiQueryKeys } from "@constants/apiConstants"; -import { isMockMode, useBackendApi } from "@api/backend/client"; +import { useBackendApi } from "@api/backend/client"; import { beStateFromUi, priorityFromSeverity } from "@api/backend/mappers"; import type { BeCaseSearchPayload, BeCaseSearchResponse, } from "@api/backend/types"; -import { getMockCsmCases } from "@features/csm-cases/api/mocks/casesMocks"; import { MATRIX_SEVERITIES, MATRIX_STATES, @@ -60,8 +59,7 @@ export interface CaseComposition { * The backend has no aggregation endpoint, so — like the severity×state matrix * — this fans out count-only searches (`limit: 1`, read `total`): one per * severity (filtered by priority AND the active states) and one per active - * state (filtered by state), plus one for the closed total. MOCK mode tallies - * the seeded cases. + * state (filtered by state), plus one for the closed total. * * Severity and state each partition the same active population, so both totals * are equal and reconcile with the matrix's grand total. @@ -78,74 +76,61 @@ export function useCaseComposition(): UseQueryResult { COMPOSITION_STATES.forEach((s) => (byState[s] = 0)); let closedTotal = 0; - if (isMockMode()) { - for (const c of getMockCsmCases("all_customers")) { - // Closed cases are tallied on their own and kept out of the pies so - // the active counts reconcile with the matrix above. - if (c.state === "closed") { - closedTotal += 1; - continue; - } - if (bySeverity[c.severity] !== undefined) bySeverity[c.severity] += 1; - if (byState[c.state] !== undefined) byState[c.state] += 1; - } - } else { - const countTotal = (payload: BeCaseSearchPayload): Promise => - api - .post( - "/cases/search", - payload, - ) - .then((r) => r.total ?? 0); + const countTotal = (payload: BeCaseSearchPayload): Promise => + api + .post( + "/cases/search", + payload, + ) + .then((r) => r.total ?? 0); - // The active states, expressed once for the severity counts so each is - // scoped to active cases (priority AND active-state), not the whole - // population. - const activeStateKeys = COMPOSITION_STATES.map(beStateFromUi); - // The state/closed counts don't filter by severity. The backend, - // however, counts only catastrophic cases when `severityKeys` is absent - // (an empty priority filter is not treated as "all"), which made the - // state pie show the S0 row only and disagree with the matrix totals. - // Pass every priority explicitly so these counts span all severities. - // BE follow-up: treat an absent/empty severityKeys as "all priorities". - const allPriorityKeys = MATRIX_SEVERITIES.map(priorityFromSeverity); + // The active states, expressed once for the severity counts so each is + // scoped to active cases (priority AND active-state), not the whole + // population. + const activeStateKeys = COMPOSITION_STATES.map(beStateFromUi); + // The state/closed counts don't filter by severity. The backend, + // however, counts only catastrophic cases when `severityKeys` is absent + // (an empty priority filter is not treated as "all"), which made the + // state pie show the S0 row only and disagree with the matrix totals. + // Pass every priority explicitly so these counts span all severities. + // BE follow-up: treat an absent/empty severityKeys as "all priorities". + const allPriorityKeys = MATRIX_SEVERITIES.map(priorityFromSeverity); - // All severity + state counts (plus the closed total) fire in one wave. - const [severityCounts, stateCounts, closedCount] = await Promise.all([ - Promise.all( - MATRIX_SEVERITIES.map((sev) => - countTotal({ - pagination: { offset: 0, limit: 1 }, - filters: { - severityKeys: [priorityFromSeverity(sev)], - stateKeys: activeStateKeys, - }, - }).then((n) => ({ key: sev, n })), - ), + // All severity + state counts (plus the closed total) fire in one wave. + const [severityCounts, stateCounts, closedCount] = await Promise.all([ + Promise.all( + MATRIX_SEVERITIES.map((sev) => + countTotal({ + pagination: { offset: 0, limit: 1 }, + filters: { + severityKeys: [priorityFromSeverity(sev)], + stateKeys: activeStateKeys, + }, + }).then((n) => ({ key: sev, n })), ), - Promise.all( - COMPOSITION_STATES.map((st) => - countTotal({ - pagination: { offset: 0, limit: 1 }, - filters: { - stateKeys: [beStateFromUi(st)], - severityKeys: allPriorityKeys, - }, - }).then((n) => ({ key: st, n })), - ), + ), + Promise.all( + COMPOSITION_STATES.map((st) => + countTotal({ + pagination: { offset: 0, limit: 1 }, + filters: { + stateKeys: [beStateFromUi(st)], + severityKeys: allPriorityKeys, + }, + }).then((n) => ({ key: st, n })), ), - countTotal({ - pagination: { offset: 0, limit: 1 }, - filters: { - stateKeys: [beStateFromUi("closed")], - severityKeys: allPriorityKeys, - }, - }), - ]); - severityCounts.forEach(({ key, n }) => (bySeverity[key] = n)); - stateCounts.forEach(({ key, n }) => (byState[key] = n)); - closedTotal = closedCount; - } + ), + countTotal({ + pagination: { offset: 0, limit: 1 }, + filters: { + stateKeys: [beStateFromUi("closed")], + severityKeys: allPriorityKeys, + }, + }), + ]); + severityCounts.forEach(({ key, n }) => (bySeverity[key] = n)); + stateCounts.forEach(({ key, n }) => (byState[key] = n)); + closedTotal = closedCount; const severityTotal = MATRIX_SEVERITIES.reduce( (a, s) => a + bySeverity[s], diff --git a/apps/csm-portal/webapp/src/features/csm-dashboard/api/useCaseCountsMatrix.ts b/apps/csm-portal/webapp/src/features/csm-dashboard/api/useCaseCountsMatrix.ts index ec12ace9b1..9194947565 100644 --- a/apps/csm-portal/webapp/src/features/csm-dashboard/api/useCaseCountsMatrix.ts +++ b/apps/csm-portal/webapp/src/features/csm-dashboard/api/useCaseCountsMatrix.ts @@ -16,13 +16,12 @@ import { useQuery, type UseQueryResult } from "@tanstack/react-query"; import { ApiQueryKeys } from "@constants/apiConstants"; -import { isMockMode, useBackendApi } from "@api/backend/client"; +import { useBackendApi } from "@api/backend/client"; import { beStateFromUi, priorityFromSeverity } from "@api/backend/mappers"; import type { BeCaseSearchPayload, BeCaseSearchResponse, } from "@api/backend/types"; -import { getMockCsmCases } from "@features/csm-cases/api/mocks/casesMocks"; import type { CaseState, Severity, @@ -68,7 +67,7 @@ function emptyMatrix(): CaseCountsMatrix { * cell's priority + state and asks for `limit: 1`, then reads the `total` * attribute off the response (the case rows themselves are discarded). Counts * are therefore *exact* — no sampling, no truncation. Row/column/grand totals - * are summed from the cells. MOCK mode tallies the seeded cases client-side. + * are summed from the cells. * * Trade-off: this is `MATRIX_SEVERITIES.length * MATRIX_STATES.length` requests * (fired in parallel) per refresh. They are cheap indexed counts; the proper @@ -86,17 +85,6 @@ export function useCaseCountsMatrix(): UseQueryResult { queryFn: async (): Promise => { const matrix = emptyMatrix(); - if (isMockMode()) { - for (const c of getMockCsmCases("all_customers")) { - if (matrix.counts[c.severity]?.[c.state] === undefined) continue; - matrix.counts[c.severity][c.state] += 1; - matrix.severityTotals[c.severity] += 1; - matrix.stateTotals[c.state] += 1; - matrix.total += 1; - } - return matrix; - } - const cells = MATRIX_SEVERITIES.flatMap((severity) => MATRIX_STATES.map((state) => ({ severity, state })), ); diff --git a/apps/csm-portal/webapp/src/features/csm-dashboard/api/useGetCsmDashboard.ts b/apps/csm-portal/webapp/src/features/csm-dashboard/api/useGetCsmDashboard.ts index d2e9fdc2d1..3d50d3b2bd 100644 --- a/apps/csm-portal/webapp/src/features/csm-dashboard/api/useGetCsmDashboard.ts +++ b/apps/csm-portal/webapp/src/features/csm-dashboard/api/useGetCsmDashboard.ts @@ -16,40 +16,25 @@ import { useQuery, type UseQueryResult } from "@tanstack/react-query"; import { useAuthApiClient } from "@hooks/useAuthApiClient"; -import { useLogger } from "@hooks/useLogger"; import { ApiQueryKeys } from "@constants/apiConstants"; -import { getMockAbtDashboard } from "@features/csm-dashboard/api/mocks/dashboardMocks"; import type { CsmAbtDashboardData, DashboardScope, } from "@features/csm-dashboard/types/abtDashboard"; -const MOCK_LATENCY_MS = 250; - /** * Loads the CSM ABT dashboard payload. * - * When `window.config.CSM_PORTAL_USE_MOCKS` is true, the hook resolves with - * in-memory mock data so the UI can be built ahead of csm-portal/backend. - * Otherwise it calls `GET ${BACKEND_BASE_URL}/csm/dashboard?scope=...`. + * Calls `GET ${BACKEND_BASE_URL}/csm/dashboard?scope=...`. */ export function useGetCsmDashboard( scope: DashboardScope, ): UseQueryResult { - const logger = useLogger(); const authFetch = useAuthApiClient(); return useQuery({ queryKey: [ApiQueryKeys.CSM_ABT_DASHBOARD, scope], queryFn: async (): Promise => { - if (window.config?.CSM_PORTAL_USE_MOCKS) { - logger.debug( - `[useGetCsmDashboard] Returning mock data for scope=${scope}`, - ); - await new Promise((r) => setTimeout(r, MOCK_LATENCY_MS)); - return getMockAbtDashboard(scope); - } - const baseUrl = window.config?.CSM_PORTAL_BACKEND_BASE_URL; if (!baseUrl) { throw new Error("CSM_PORTAL_BACKEND_BASE_URL is not configured"); diff --git a/apps/csm-portal/webapp/src/features/csm-dashboard/components/MyQueueSection.tsx b/apps/csm-portal/webapp/src/features/csm-dashboard/components/MyQueueSection.tsx deleted file mode 100644 index c231fc35f7..0000000000 --- a/apps/csm-portal/webapp/src/features/csm-dashboard/components/MyQueueSection.tsx +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -import { Box, Skeleton, Typography, useTheme } from "@wso2/oxygen-ui"; -import type { JSX, KeyboardEvent } from "react"; -import { useNavigate } from "react-router"; -import SectionCard from "@features/csm-dashboard/components/SectionCard"; -import { - SLA_CLOCK_LABEL, - formatTimeToBreach, - stateLabel, -} from "@features/csm-dashboard/utils/abtDashboard"; -import SeverityChip from "@components/SeverityChip"; -import { casesHref } from "@features/csm-cases/utils/casesFiltersUrl"; -import { ASSIGNEE_ME_TOKEN } from "@features/csm-cases/components/CasesFilterBar"; -import type { CsmQueueSummary } from "@features/csm-dashboard/types/abtDashboard"; - -interface MyQueueSectionProps { - queue?: CsmQueueSummary; - isLoading: boolean; - isError?: boolean; -} - -function StatPill({ - label, - value, - href, - onNavigate, -}: { - label: string; - value: number | string; - href?: string; - onNavigate?: (href: string) => void; -}): JSX.Element { - const interactive = !!href && !!onNavigate; - const handleKey = (e: KeyboardEvent) => { - if (!interactive) return; - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - onNavigate!(href!); - } - }; - return ( - onNavigate!(href!) : undefined} - onKeyDown={handleKey} - sx={{ - flex: 1, - minWidth: 120, - p: 1.5, - borderRadius: 1, - border: 1, - borderColor: "divider", - display: "flex", - flexDirection: "column", - gap: 0.5, - ...(interactive && { - cursor: "pointer", - transition: "border-color 0.15s ease, background-color 0.15s ease", - "&:hover": { - borderColor: "primary.main", - bgcolor: "action.hover", - }, - }), - }} - > - {value} - - {label} - - - ); -} - -export default function MyQueueSection({ - queue, - isLoading, - isError, -}: MyQueueSectionProps): JSX.Element { - const navigate = useNavigate(); - const theme = useTheme(); - const go = (href: string) => navigate(href); - - if (isError) { - return ( - - - Couldn’t load your queue right now. - - - ); - } - - return ( - - - - - - - - - - Most urgent - {isLoading && ( - <> - - - - - )} - {!isLoading && (queue?.topCases ?? []).length === 0 && ( - - Nothing in your queue. - - )} - {!isLoading && - queue?.topCases.map((c) => ( - go(casesHref({ search: c.caseNumber }))} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - go(casesHref({ search: c.caseNumber })); - } - }} - sx={{ - display: "flex", - alignItems: "center", - gap: 1.5, - p: 1.25, - border: 1, - borderColor: "divider", - borderRadius: 1, - cursor: "pointer", - transition: "border-color 0.15s ease, background-color 0.15s ease", - "&:hover": { - borderColor: "primary.main", - bgcolor: "action.hover", - }, - "&:focus-visible": { - outline: `2px solid ${theme.palette.primary.main}`, - outlineOffset: 2, - }, - }} - > - - - - {c.caseNumber} · {c.subject} - - - {c.customer} · {stateLabel(c.state)} - - - - - {formatTimeToBreach(c.minutesToBreach)} - - - {SLA_CLOCK_LABEL[c.slaClockType]} - - - - ))} - - - ); -} diff --git a/apps/csm-portal/webapp/src/features/csm-dashboard/components/SlaAtRiskSection.tsx b/apps/csm-portal/webapp/src/features/csm-dashboard/components/SlaAtRiskSection.tsx deleted file mode 100644 index 6b7da0345c..0000000000 --- a/apps/csm-portal/webapp/src/features/csm-dashboard/components/SlaAtRiskSection.tsx +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright (c) 2026 WSO2 LLC. (https://www.wso2.com). -// -// WSO2 LLC. licenses this file to you under the Apache License, -// Version 2.0 (the "License"); you may not use this file except -// in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -import { Box, Chip, Skeleton, Typography, useTheme } from "@wso2/oxygen-ui"; -import type { JSX } from "react"; -import { useNavigate } from "react-router"; -import SectionCard from "@features/csm-dashboard/components/SectionCard"; -import { - SLA_CLOCK_LABEL, - formatTimeToBreach, - stateLabel, -} from "@features/csm-dashboard/utils/abtDashboard"; -import SeverityChip from "@components/SeverityChip"; -import { casesHref } from "@features/csm-cases/utils/casesFiltersUrl"; -import type { CsmSlaAtRiskCase } from "@features/csm-dashboard/types/abtDashboard"; - -interface SlaAtRiskSectionProps { - cases?: CsmSlaAtRiskCase[]; - isLoading: boolean; - isError?: boolean; -} - -export default function SlaAtRiskSection({ - cases, - isLoading, - isError, -}: SlaAtRiskSectionProps): JSX.Element { - const navigate = useNavigate(); - const theme = useTheme(); - const go = (href: string) => navigate(href); - const breachedCount = (cases ?? []).filter((c) => c.minutesToBreach < 0).length; - - if (isError) { - return ( - - - Couldn’t load SLA data right now. - - - ); - } - - return ( - 0 ? ( - 0 ? "error" : "warning"} - label={ - breachedCount > 0 - ? `${breachedCount} breached` - : `${cases?.length ?? 0} at risk` - } - onClick={() => - go( - casesHref({ - sla: breachedCount > 0 ? "breached" : "at_risk", - }), - ) - } - /> - ) : undefined - } - > - - {isLoading && ( - <> - - - - - )} - {!isLoading && (cases?.length ?? 0) === 0 && ( - - Nothing at risk right now. - - )} - {!isLoading && - cases?.map((c) => ( - go(casesHref({ search: c.caseNumber }))} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - go(casesHref({ search: c.caseNumber })); - } - }} - sx={{ - display: "flex", - alignItems: "center", - gap: 1.5, - p: 1.25, - border: 1, - borderColor: c.minutesToBreach < 0 ? "error.main" : "divider", - borderRadius: 1, - cursor: "pointer", - transition: "border-color 0.15s ease, background-color 0.15s ease", - "&:hover": { - bgcolor: "action.hover", - ...(c.minutesToBreach >= 0 && { borderColor: "primary.main" }), - }, - "&:focus-visible": { - outline: `2px solid ${theme.palette.primary.main}`, - outlineOffset: 2, - }, - }} - > - - - - {c.caseNumber} · {c.subject} - - - {c.customer} · {stateLabel(c.state)} · Assignee: {c.assignee} - - - - - {formatTimeToBreach(c.minutesToBreach)} - - - {SLA_CLOCK_LABEL[c.slaClockType]} - - - - ))} - - - ); -} diff --git a/apps/csm-portal/webapp/src/main.tsx b/apps/csm-portal/webapp/src/main.tsx index 9022d4ba78..b090a3bbea 100644 --- a/apps/csm-portal/webapp/src/main.tsx +++ b/apps/csm-portal/webapp/src/main.tsx @@ -18,17 +18,12 @@ import Prism from "prismjs"; import React from "react"; import { createRoot } from "react-dom/client"; import AppWithConfig from "./AppWithConfig"; -import { hydrateMockModeFromStorage } from "@context/mock-mode/MockModeContext"; import { POST_LOGIN_REDIRECT_KEY } from "@layouts/postLoginRedirect"; if (typeof window !== "undefined") { (window as unknown as { Prism: typeof Prism }).Prism = Prism; } -// Apply any saved mock-vs-backend preference to window.config BEFORE React -// mounts, so the first render of every data hook reads the correct flag. -hydrateMockModeFromStorage(); - // Capture the entry deep link at module load — before React, routing, or the // Asgardeo SDK run — so it can be restored after the IdP round-trip even when // the SDK restores the session synchronously (in which case ProtectedRoute's diff --git a/apps/csm-portal/webapp/vite.config.ts b/apps/csm-portal/webapp/vite.config.ts index a8970402a0..aa5a7bc1c2 100644 --- a/apps/csm-portal/webapp/vite.config.ts +++ b/apps/csm-portal/webapp/vite.config.ts @@ -98,7 +98,7 @@ const viteConfig = defineConfig({ }, envPrefix: ["CSM_PORTAL_"], server: { - port: 3000, + port: 3001, strictPort: true, }, }); @@ -159,10 +159,6 @@ window.config.CSM_PORTAL_BACKEND_BASE_URL = window.location.origin + "/local-api // loops on sign-in. Same same-origin rationale as the backend URL above. window.config.CSM_PORTAL_AUTH_SIGN_IN_REDIRECT_URL = window.location.origin; window.config.CSM_PORTAL_AUTH_SIGN_OUT_REDIRECT_URL = window.location.origin; -window.config.CSM_PORTAL_USE_MOCKS = false; -// Pin the runtime mock toggle off so a stale localStorage flag cannot -// silently re-enable mock data in front of the real local backend. -try { localStorage.setItem("csm.useMocks", "0"); } catch (e) { /* ignore */ } `; function localGatewayConfigPlugin(): Plugin {