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 &&