From 836f601ce683c23f39e8ab0c9e3dfafbd62c41db Mon Sep 17 00:00:00 2001 From: abhibhaw <39991296+abhibhaw@users.noreply.github.com> Date: Mon, 16 Mar 2026 23:27:43 +0530 Subject: [PATCH 1/4] feat: Claude init --- CLAUDE.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 42d32bcd55..974a6820c0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -522,3 +522,81 @@ import { Icon } from '@devtron-labs/devtron-fe-common-lib' - **Purpose**: Primary web application for Devtron platform - **Type**: SPA (Single Page Application) - not a library - **Deployment**: Serves as the frontend for Kubernetes-native CI/CD platform + +## EA Mode (Hyperion) Frontend Behavior + +> **Deep reference**: See [EA_MODE_REFERENCE.md](../EA_MODE_REFERENCE.md) Section G for full frontend-backend mapping. + +### Server Mode Detection + +```typescript +// Runtime environment variable set by backend +window._env_.serverMode // 'EA_ONLY' | 'FULL' + +// Common patterns for mode checking: +import { ServerMode } from '@devtron-labs/devtron-fe-common-lib' + +// Check if running in EA mode +const isEAMode = window._env_.serverMode === ServerMode.EA_ONLY +// or +const isEAMode = window._env_.serverMode === 'EA_ONLY' +``` + +### EA-Visible Routes + +These routes are available in EA mode: +- `/dashboard` — Main dashboard +- `/dashboard/cluster-list` — Cluster management +- `/dashboard/chart-store` — Helm chart catalog (App Store) +- `/dashboard/external-apps` — External Helm app list +- `/dashboard/resource-browser` — K8s resource browser +- `/dashboard/global-config` — Platform settings (SSO, auth, repos) +- `/dashboard/security` — Security scanning results + +### EA-Hidden Routes (Full Mode Only) + +These routes/features are hidden or disabled in EA mode: +- App creation wizard (`/dashboard/app/create`) +- CI/CD workflow editor (`/dashboard/app/*/workflow-editor`) +- CI build details (`/dashboard/app/*/ci-details`) +- CD deployment details (`/dashboard/app/*/cd-details`) +- Pipeline triggers (`/dashboard/app/*/trigger`) +- Bulk edit operations (`/dashboard/bulk-edits`) +- Notification settings (`/dashboard/notifications`) +- GitOps configuration (`/dashboard/global-config/gitops`) + +### Conditional Rendering Pattern + +Components check server mode to show/hide features: + +```typescript +// In component or page +const { serverMode } = window._env_ + +if (serverMode === 'EA_ONLY') { + // Show EA-specific UI (external apps, chart store) + return +} +// Show full-mode UI (devtron apps, pipelines) +return +``` + +### EA-Specific Components + +Key components used primarily in EA mode: +- `src/components/hyperion/` — EA-specific features +- External app management components +- Chart store browsing and deployment +- Cluster/resource browser (shared with full mode) + +### API Proxy + +All API calls proxy through Vite dev server to `/orchestrator/*`: +``` +Frontend → /orchestrator/application → Backend :8080 +Frontend → /orchestrator/app-store/* → Backend :8080 +Frontend → /orchestrator/cluster → Backend :8080 +Frontend → /orchestrator/k8s/* → Backend :8080 +``` + +The backend router in EA mode (`cmd/external-app/router.go`) only registers EA-relevant endpoints, so full-mode API calls will 404 in EA deployments From 6fbf203abb53d6f5943fd31009920ba65d523eea Mon Sep 17 00:00:00 2001 From: abhibhaw <39991296+abhibhaw@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:57:43 +0530 Subject: [PATCH 2/4] feat: add ETag/If-None-Match support for resource tree polling Phase 3 of resource tree rewrite for dashboard: - Add ETag header tracking in useGetDTAppDetails resource tree query - Send If-None-Match header on polls, skip IndexStore update on 304 - Revert poll interval to 30s (safe default until NATS pipeline is validated) Co-Authored-By: Claude Opus 4.6 --- src/components/app/service.ts | 38 ++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/components/app/service.ts b/src/components/app/service.ts index e900b41f9b..47118e2183 100644 --- a/src/components/app/service.ts +++ b/src/components/app/service.ts @@ -16,6 +16,8 @@ import moment from 'moment' +import { useRef } from 'react' + import { ACTION_STATE, APIOptions, @@ -78,6 +80,7 @@ export const getAppList = (request, options?: APIOptions) => post(Routes.APP_LIS export const useGetDTAppDetails = ({ appId, envId }: UseGetDTAppDetailsParams): UseGetDTAppDetailsReturnType => { const queryClient = useQueryClient() const resourceTreeQueryKey = 'dt-app-resource-tree' + const resourceTreeETagRef = useRef('') const { data: appDetails, @@ -120,11 +123,36 @@ export const useGetDTAppDetails = ({ appId, envId }: UseGetDTAppDetailsParams): status: resourceTreeQueryStatus, } = useQuery({ queryKey: [resourceTreeQueryKey, appId, envId], - queryFn: ({ signal }) => - get( - getUrlWithSearchParams(`${Routes.APP_DETAIL}/resource-tree`, { 'app-id': appId, 'env-id': envId }), - { signal }, - ), + queryFn: async ({ signal }) => { + const url = getUrlWithSearchParams(`${Routes.APP_DETAIL}/resource-tree`, { + 'app-id': appId, + 'env-id': envId, + }) + const headers: Record = {} + if (resourceTreeETagRef.current) { + headers['If-None-Match'] = resourceTreeETagRef.current + } + + const response = await fetch(`${window.__ORCHESTRATOR_ROOT__}/${url}`, { signal, headers }) + + if (response.status === 304) { + // Tree hasn't changed — return previous cached data from TanStack Query + const previousData = queryClient.getQueryData([ + resourceTreeQueryKey, + appId, + envId, + ]) + return { result: previousData } as any + } + + const newETag = response.headers.get('Etag') + if (newETag) { + resourceTreeETagRef.current = newETag + } + + const data = await response.json() + return data + }, select: ({ result }) => result, enabled: !!appId && From a7e7baff2711deaa723da14756f00bace2ec7355 Mon Sep 17 00:00:00 2001 From: abhibhaw <39991296+abhibhaw@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:53:23 +0530 Subject: [PATCH 3/4] fix: handle 304 response correctly in resource tree polling getQueryData returns raw queryFn data (pre-select), so return it directly instead of wrapping in { result: previousData } which caused select() to extract the full API envelope as the tree. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/app/service.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/app/service.ts b/src/components/app/service.ts index 47118e2183..8b873a7604 100644 --- a/src/components/app/service.ts +++ b/src/components/app/service.ts @@ -136,13 +136,19 @@ export const useGetDTAppDetails = ({ appId, envId }: UseGetDTAppDetailsParams): const response = await fetch(`${window.__ORCHESTRATOR_ROOT__}/${url}`, { signal, headers }) if (response.status === 304) { - // Tree hasn't changed — return previous cached data from TanStack Query - const previousData = queryClient.getQueryData([ - resourceTreeQueryKey, - appId, - envId, - ]) - return { result: previousData } as any + // Tree hasn't changed — return raw queryFn data so select() can unwrap it + const previousData = queryClient.getQueryData([resourceTreeQueryKey, appId, envId]) + if (previousData) { + return previousData + } + // No cached data yet — retry without ETag + resourceTreeETagRef.current = '' + const fallbackResponse = await fetch(`${window.__ORCHESTRATOR_ROOT__}/${url}`, { signal }) + const fallbackETag = fallbackResponse.headers.get('Etag') + if (fallbackETag) { + resourceTreeETagRef.current = fallbackETag + } + return fallbackResponse.json() } const newETag = response.headers.get('Etag') From 49a951e52d52a087c015e40621ecd188eba64999 Mon Sep 17 00:00:00 2001 From: abhibhaw <39991296+abhibhaw@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:56:26 +0530 Subject: [PATCH 4/4] fix: revert raw fetch() to use get() for resource tree polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Raw fetch() bypassed CoreAPI, breaking auth (missing credentials: 'include'), error handling (401/403/500), and license validation in production. ETag/304 optimization removed — incompatible with CoreAPI without extending APIOptions in devtron-fe-common-lib first. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/app/service.ts | 44 ++++------------------------------- 1 file changed, 5 insertions(+), 39 deletions(-) diff --git a/src/components/app/service.ts b/src/components/app/service.ts index 8b873a7604..bf0a226f0c 100644 --- a/src/components/app/service.ts +++ b/src/components/app/service.ts @@ -16,7 +16,6 @@ import moment from 'moment' -import { useRef } from 'react' import { ACTION_STATE, @@ -80,8 +79,6 @@ export const getAppList = (request, options?: APIOptions) => post(Routes.APP_LIS export const useGetDTAppDetails = ({ appId, envId }: UseGetDTAppDetailsParams): UseGetDTAppDetailsReturnType => { const queryClient = useQueryClient() const resourceTreeQueryKey = 'dt-app-resource-tree' - const resourceTreeETagRef = useRef('') - const { data: appDetails, isFetching: isFetchingAppDetails, @@ -123,42 +120,11 @@ export const useGetDTAppDetails = ({ appId, envId }: UseGetDTAppDetailsParams): status: resourceTreeQueryStatus, } = useQuery({ queryKey: [resourceTreeQueryKey, appId, envId], - queryFn: async ({ signal }) => { - const url = getUrlWithSearchParams(`${Routes.APP_DETAIL}/resource-tree`, { - 'app-id': appId, - 'env-id': envId, - }) - const headers: Record = {} - if (resourceTreeETagRef.current) { - headers['If-None-Match'] = resourceTreeETagRef.current - } - - const response = await fetch(`${window.__ORCHESTRATOR_ROOT__}/${url}`, { signal, headers }) - - if (response.status === 304) { - // Tree hasn't changed — return raw queryFn data so select() can unwrap it - const previousData = queryClient.getQueryData([resourceTreeQueryKey, appId, envId]) - if (previousData) { - return previousData - } - // No cached data yet — retry without ETag - resourceTreeETagRef.current = '' - const fallbackResponse = await fetch(`${window.__ORCHESTRATOR_ROOT__}/${url}`, { signal }) - const fallbackETag = fallbackResponse.headers.get('Etag') - if (fallbackETag) { - resourceTreeETagRef.current = fallbackETag - } - return fallbackResponse.json() - } - - const newETag = response.headers.get('Etag') - if (newETag) { - resourceTreeETagRef.current = newETag - } - - const data = await response.json() - return data - }, + queryFn: ({ signal }) => + get( + getUrlWithSearchParams(`${Routes.APP_DETAIL}/resource-tree`, { 'app-id': appId, 'env-id': envId }), + { signal }, + ), select: ({ result }) => result, enabled: !!appId &&