Skip to content

Commit 4b1a0cf

Browse files
jsell-rhclaude
andauthored
feat(ambient-ui): walking skeleton with BFF auth, fleet view, and deployment (#1612)
## Summary - **New component**: `components/ambient-ui/` — Next.js BFF ops console implementing the ambient-ui spec's walking skeleton - **Auth**: Dual-mode OIDC (native-sso/oauth-proxy/none) with iron-session, JWT relay to ambient-api-server - **Architecture**: Port/adapter API layer decoupling domain types from generated TypeScript SDK - **Fleet view**: Session table with Phase/Name/Agent/Duration/Model/Activity/Cost columns, adaptive polling, session detail with Phase tab - **Deployment**: Red Hat Hardened Image (distroless) Dockerfile, kustomize manifests (base + kind-local + production with oauth-proxy sidecar), Makefile targets - **CI**: Unit tests in `unit-tests.yml`, build in `prod-release-deploy.yaml` - **42 tests** across adapters, mappers, hooks, observability, and utilities ## Test plan - [ ] `cd components/ambient-ui && npm run build` — 0 errors, 0 warnings - [ ] `cd components/ambient-ui && npm run test` — 42 tests pass - [ ] `cd components/ambient-ui && npm run lint` — 0 errors - [ ] `kubectl kustomize components/manifests/overlays/production/` — renders ambient-ui deployment, service, route - [ ] `kubectl kustomize components/manifests/overlays/kind-local/` — renders ambient-ui with localhost image - [ ] Local dev: `npm run dev` on port 3001 with ambient-api-server running — project picker, fleet table, session detail all functional - [ ] Verify oauth-proxy sidecar config matches existing frontend pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a new web-based dashboard for viewing and managing projects and sessions. * Integrated single sign-on (SSO) and OAuth proxy authentication options. * Dashboard includes project selection, fleet/session overview with filtering, and detailed session information with phase tracking and metadata. * **Build & Deployment** * Added Docker containerization and Kubernetes manifests for the new component. * Updated CI/CD workflows to include the new component in release builds and automated testing. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent f4f06dc commit 4b1a0cf

92 files changed

Lines changed: 15933 additions & 8 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/prod-release-deploy.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ on:
1818
type: boolean
1919
default: true
2020
components:
21-
description: 'Components to build (comma-separated: frontend,backend,operator,ambient-runner,state-sync,public-api,ambient-api-server,ambient-control-plane,ambient-mcp) - leave empty for all'
21+
description: 'Components to build (comma-separated: frontend,backend,operator,ambient-runner,state-sync,public-api,ambient-api-server,ambient-control-plane,ambient-mcp,ambient-ui) - leave empty for all'
2222
required: false
2323
type: string
2424
default: ''
@@ -238,7 +238,8 @@ jobs:
238238
{"name":"public-api","context":"./components/public-api","image":"quay.io/ambient_code/vteam_public_api","dockerfile":"./components/public-api/Dockerfile"},
239239
{"name":"ambient-api-server","context":"./components/ambient-api-server","image":"quay.io/ambient_code/vteam_api_server","dockerfile":"./components/ambient-api-server/Dockerfile"},
240240
{"name":"ambient-control-plane","context":"./components","image":"quay.io/ambient_code/vteam_control_plane","dockerfile":"./components/ambient-control-plane/Dockerfile"},
241-
{"name":"ambient-mcp","context":"./components/ambient-mcp","image":"quay.io/ambient_code/vteam_mcp","dockerfile":"./components/ambient-mcp/Dockerfile"}
241+
{"name":"ambient-mcp","context":"./components/ambient-mcp","image":"quay.io/ambient_code/vteam_mcp","dockerfile":"./components/ambient-mcp/Dockerfile"},
242+
{"name":"ambient-ui","context":"./components","image":"quay.io/ambient_code/vteam_ambient_ui","dockerfile":"./components/ambient-ui/Dockerfile"}
242243
]'
243244
244245
FORCE_ALL="${{ github.event.inputs.force_build_all }}"
@@ -624,6 +625,7 @@ jobs:
624625
["public-api"]="public-api:public-api"
625626
["ambient-api-server"]="ambient-api-server:ambient-api-server"
626627
["ambient-control-plane"]="ambient-control-plane:ambient-control-plane"
628+
["ambient-ui"]="ambient-ui:ambient-ui"
627629
)
628630
629631
@@ -636,7 +638,8 @@ jobs:
636638
"public-api:quay.io/ambient_code/vteam_public_api" \
637639
"ambient-api-server:quay.io/ambient_code/vteam_api_server" \
638640
"ambient-control-plane:quay.io/ambient_code/vteam_control_plane" \
639-
"ambient-mcp:quay.io/ambient_code/vteam_mcp"; do
641+
"ambient-mcp:quay.io/ambient_code/vteam_mcp" \
642+
"ambient-ui:quay.io/ambient_code/vteam_ambient_ui"; do
640643
COMP="${comp_image%%:*}"
641644
IMAGE="${comp_image#*:}"
642645

.github/workflows/unit-tests.yml

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
name: Unit Tests
22

3+
permissions:
4+
contents: read
5+
36
on:
47
push:
58
branches: [main]
@@ -10,6 +13,7 @@ on:
1013
- 'components/ambient-cli/**'
1114
- 'components/ambient-sdk/go-sdk/**'
1215
- 'components/frontend/**'
16+
- 'components/ambient-ui/**'
1317
- '.github/scripts/**'
1418
- 'tests/**'
1519
- '.github/workflows/unit-tests.yml'
@@ -23,6 +27,7 @@ on:
2327
- 'components/ambient-cli/**'
2428
- 'components/ambient-sdk/go-sdk/**'
2529
- 'components/frontend/**'
30+
- 'components/ambient-ui/**'
2631
- '.github/scripts/**'
2732
- 'tests/**'
2833
- '.github/workflows/unit-tests.yml'
@@ -54,6 +59,7 @@ jobs:
5459
runner: ${{ steps.filter.outputs.runner }}
5560
cli: ${{ steps.filter.outputs.cli }}
5661
frontend: ${{ steps.filter.outputs.frontend }}
62+
ambient-ui: ${{ steps.filter.outputs.ambient-ui }}
5763
scripts: ${{ steps.filter.outputs.scripts }}
5864
steps:
5965
- name: Checkout code
@@ -75,6 +81,8 @@ jobs:
7581
- 'components/ambient-sdk/go-sdk/**'
7682
frontend:
7783
- 'components/frontend/**'
84+
ambient-ui:
85+
- 'components/ambient-ui/**'
7886
scripts:
7987
- '.github/scripts/**'
8088
- 'tests/test_model_discovery.py'
@@ -307,6 +315,36 @@ jobs:
307315
- name: Run unit tests with coverage
308316
run: npx vitest run --coverage
309317

318+
ambient-ui:
319+
runs-on: ubuntu-latest
320+
needs: detect-changes
321+
if: needs.detect-changes.outputs.ambient-ui == 'true' || github.event_name == 'workflow_dispatch'
322+
name: Ambient UI Unit Tests (Vitest)
323+
defaults:
324+
run:
325+
working-directory: components/ambient-ui
326+
327+
steps:
328+
- name: Checkout code
329+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
330+
331+
- name: Set up Node.js
332+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
333+
with:
334+
node-version-file: 'components/ambient-ui/package.json'
335+
cache: 'npm'
336+
cache-dependency-path: 'components/ambient-ui/package-lock.json'
337+
338+
- name: Build ambient-sdk (file: dependency)
339+
working-directory: components/ambient-sdk/ts-sdk
340+
run: npm ci && npm run build
341+
342+
- name: Install dependencies
343+
run: npm ci
344+
345+
- name: Run unit tests with coverage
346+
run: npx vitest run --coverage
347+
310348
scripts:
311349
runs-on: ubuntu-latest
312350
needs: detect-changes
@@ -326,7 +364,7 @@ jobs:
326364

327365
summary:
328366
runs-on: ubuntu-latest
329-
needs: [detect-changes, backend, api-server, runner, cli, frontend, scripts]
367+
needs: [detect-changes, backend, api-server, runner, cli, frontend, ambient-ui, scripts]
330368
if: always()
331369
steps:
332370
- name: Check overall status
@@ -338,6 +376,7 @@ jobs:
338376
"${{ needs.runner.result }}" \
339377
"${{ needs.cli.result }}" \
340378
"${{ needs.frontend.result }}" \
379+
"${{ needs.ambient-ui.result }}" \
341380
"${{ needs.scripts.result }}"; do
342381
if [ "$result" == "failure" ] || [ "$result" == "cancelled" ]; then
343382
failed=true
@@ -350,6 +389,7 @@ jobs:
350389
echo " runner: ${{ needs.runner.result }}"
351390
echo " cli: ${{ needs.cli.result }}"
352391
echo " frontend: ${{ needs.frontend.result }}"
392+
echo " ambient-ui: ${{ needs.ambient-ui.result }}"
353393
echo " scripts: ${{ needs.scripts.result }}"
354394
exit 1
355395
fi

Makefile

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
.PHONY: help setup build-all build-frontend build-backend build-operator build-runner build-state-sync build-public-api build-cli deploy clean check-architecture
1+
.PHONY: help setup build-all build-frontend build-backend build-operator build-runner build-state-sync build-public-api build-cli build-ambient-ui deploy clean check-architecture
22
.PHONY: local-down local-status local-reload-api-server local-up local-clean local-rebuild local-reload-backend local-reload-frontend local-reload-operator
33
.PHONY: local-dev-token
44
.PHONY: local-logs local-logs-backend local-logs-frontend local-logs-operator local-shell local-shell-frontend
55
.PHONY: local-test local-test-dev local-test-quick test-all local-troubleshoot local-port-forward local-stop-port-forward
6-
.PHONY: push-all registry-login setup-hooks remove-hooks lint check-minikube check-kind check-kubectl check-local-context dev-bootstrap kind-rebuild kind-reload-backend kind-reload-frontend kind-reload-operator kind-status kind-login kind-sso-toggle
6+
.PHONY: push-all registry-login setup-hooks remove-hooks lint check-minikube check-kind check-kubectl check-local-context dev-bootstrap kind-rebuild kind-reload-backend kind-reload-frontend kind-reload-operator kind-reload-ambient-ui kind-status kind-login kind-sso-toggle
77
.PHONY: preflight-cluster preflight dev-env dev
88
.PHONY: e2e-test e2e-setup e2e-clean deploy-langfuse-openshift
99
.PHONY: unleash-port-forward unleash-status
@@ -72,6 +72,7 @@ GITHUB_MCP_IMAGE ?= vteam_credential_github:$(IMAGE_TAG)
7272
JIRA_MCP_IMAGE ?= vteam_credential_jira:$(IMAGE_TAG)
7373
K8S_MCP_IMAGE ?= vteam_credential_k8s:$(IMAGE_TAG)
7474
GOOGLE_MCP_IMAGE ?= vteam_credential_google:$(IMAGE_TAG)
75+
AMBIENT_UI_IMAGE ?= vteam_ambient_ui:$(IMAGE_TAG)
7576

7677
# kind-local overlay always references localhost/vteam_* images.
7778
# Podman produces this prefix natively; for Docker we tag before loading.
@@ -168,7 +169,7 @@ help: ## Display this help message
168169

169170
##@ Building
170171

171-
build-all: build-frontend build-backend build-operator build-runner build-state-sync build-public-api build-api-server build-observability-dashboard ## Build all container images
172+
build-all: build-frontend build-backend build-operator build-runner build-state-sync build-public-api build-api-server build-observability-dashboard build-ambient-ui ## Build all container images
172173

173174
build-frontend: ## Build frontend image
174175
@echo "$(COLOR_BLUE)$(COLOR_RESET) Building frontend with $(CONTAINER_ENGINE)..."
@@ -177,6 +178,14 @@ build-frontend: ## Build frontend image
177178
-t $(FRONTEND_IMAGE) .
178179
@echo "$(COLOR_GREEN)$(COLOR_RESET) Frontend built: $(FRONTEND_IMAGE)"
179180

181+
build-ambient-ui: ## Build ambient-ui image
182+
@echo "$(COLOR_BLUE)$(COLOR_RESET) Building ambient-ui with $(CONTAINER_ENGINE)..."
183+
@cd components && $(CONTAINER_ENGINE) build $(PLATFORM_FLAG) $(BUILD_FLAGS) \
184+
-f ambient-ui/Dockerfile \
185+
--build-arg GIT_COMMIT=$(shell git rev-parse HEAD) \
186+
-t $(AMBIENT_UI_IMAGE) .
187+
@echo "$(COLOR_GREEN)$(COLOR_RESET) Ambient UI built: $(AMBIENT_UI_IMAGE)"
188+
180189
build-backend: ## Build backend image
181190
@echo "$(COLOR_BLUE)$(COLOR_RESET) Building backend with $(CONTAINER_ENGINE)..."
182191
@cd components/backend && $(CONTAINER_ENGINE) build $(PLATFORM_FLAG) $(BUILD_FLAGS) \
@@ -1085,6 +1094,22 @@ kind-reload-frontend: check-kind check-kubectl check-local-context ## Rebuild an
10851094
@kubectl rollout status deployment/frontend -n $(NAMESPACE) --timeout=60s
10861095
@echo "$(COLOR_GREEN)$(COLOR_RESET) Frontend reloaded"
10871096

1097+
kind-reload-ambient-ui: check-kind check-kubectl check-local-context ## Rebuild and reload ambient-ui only (kind)
1098+
@echo "$(COLOR_BLUE)$(COLOR_RESET) Rebuilding ambient-ui..."
1099+
@cd components && $(CONTAINER_ENGINE) build $(PLATFORM_FLAG) \
1100+
-f ambient-ui/Dockerfile \
1101+
--build-arg GIT_COMMIT=$(shell git rev-parse HEAD) \
1102+
-t $(AMBIENT_UI_IMAGE) . $(QUIET_REDIRECT)
1103+
@$(CONTAINER_ENGINE) tag $(AMBIENT_UI_IMAGE) localhost/$(AMBIENT_UI_IMAGE) 2>/dev/null || true
1104+
@echo "$(COLOR_BLUE)$(COLOR_RESET) Loading image into kind cluster ($(KIND_CLUSTER_NAME))..."
1105+
@$(CONTAINER_ENGINE) save localhost/$(AMBIENT_UI_IMAGE) | \
1106+
$(CONTAINER_ENGINE) exec -i $(KIND_CLUSTER_NAME)-control-plane \
1107+
ctr --namespace=k8s.io images import -
1108+
@echo "$(COLOR_BLUE)$(COLOR_RESET) Restarting ambient-ui..."
1109+
@kubectl rollout restart deployment/ambient-ui -n $(NAMESPACE) $(QUIET_REDIRECT)
1110+
@kubectl rollout status deployment/ambient-ui -n $(NAMESPACE) --timeout=60s
1111+
@echo "$(COLOR_GREEN)$(COLOR_RESET) Ambient UI reloaded"
1112+
10881113
kind-reload-operator: check-kind check-kubectl check-local-context ## Rebuild and reload operator only (kind)
10891114
@echo "$(COLOR_BLUE)$(COLOR_RESET) Rebuilding operator..."
10901115
@cd components/operator && $(CONTAINER_ENGINE) build $(PLATFORM_FLAG) \
@@ -1300,7 +1325,7 @@ check-architecture: ## Validate build architecture matches host
13001325

13011326
_kind-load-images: ## Internal: Load images into kind cluster
13021327
@echo "$(COLOR_BLUE)$(COLOR_RESET) Loading images into kind ($(KIND_CLUSTER_NAME))..."
1303-
@for img in $(BACKEND_IMAGE) $(FRONTEND_IMAGE) $(OPERATOR_IMAGE) $(RUNNER_IMAGE) $(STATE_SYNC_IMAGE) $(PUBLIC_API_IMAGE) $(API_SERVER_IMAGE) $(OBSERVABILITY_DASHBOARD_IMAGE); do \
1328+
@for img in $(BACKEND_IMAGE) $(FRONTEND_IMAGE) $(OPERATOR_IMAGE) $(RUNNER_IMAGE) $(STATE_SYNC_IMAGE) $(PUBLIC_API_IMAGE) $(API_SERVER_IMAGE) $(OBSERVABILITY_DASHBOARD_IMAGE) $(AMBIENT_UI_IMAGE); do \
13041329
echo " Loading $(KIND_IMAGE_PREFIX)$$img..."; \
13051330
if [ -n "$(KIND_HOST)" ] || [ "$(CONTAINER_ENGINE)" = "podman" ]; then \
13061331
$(CONTAINER_ENGINE) save $(KIND_IMAGE_PREFIX)$$img | \

components/ambient-ui/.gitignore

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# dependencies
2+
/node_modules
3+
/.pnp
4+
.pnp.*
5+
__pycache__
6+
7+
# testing
8+
/coverage
9+
10+
# next.js
11+
/.next/
12+
/out/
13+
14+
# production
15+
/build
16+
17+
# misc
18+
.DS_Store
19+
*.pem
20+
21+
# debug
22+
npm-debug.log*
23+
yarn-debug.log*
24+
yarn-error.log*
25+
.pnpm-debug.log*
26+
27+
# env files
28+
.env*.local
29+
30+
# vercel
31+
.vercel
32+
33+
# typescript
34+
*.tsbuildinfo
35+
next-env.d.ts

components/ambient-ui/Dockerfile

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Use Red Hat UBI Node.js 20 minimal image for dependencies
2+
FROM registry.access.redhat.com/ubi9/nodejs-20-minimal AS deps
3+
4+
WORKDIR /app
5+
6+
USER 0
7+
8+
# Copy SDK dependency first (it's a file: dependency in package.json)
9+
COPY ambient-sdk/ts-sdk ./ambient-sdk/ts-sdk
10+
11+
# Copy ambient-ui package files
12+
COPY ambient-ui/package.json ambient-ui/package-lock.json* ./
13+
RUN npm ci
14+
15+
# Rebuild the source code only when needed
16+
# Use the full nodejs-20 image (not minimal) for the build stage because
17+
# Next.js 16 Turbopack requires native SWC binaries that depend on glibc.
18+
FROM registry.access.redhat.com/ubi9/nodejs-20 AS builder
19+
20+
USER 0
21+
22+
WORKDIR /app
23+
24+
# Copy node_modules from deps stage
25+
COPY --from=deps /app/node_modules ./node_modules
26+
COPY --from=deps /app/ambient-sdk ./ambient-sdk
27+
COPY ambient-ui/ .
28+
29+
# Next.js collects completely anonymous telemetry data about general usage.
30+
# Learn more here: https://nextjs.org/telemetry
31+
ENV NEXT_TELEMETRY_DISABLED=1
32+
33+
RUN npm run build
34+
35+
# Prepare standalone output with OpenShift-compatible permissions in the builder
36+
# (the hardened runner image is distroless — no shell or chmod available)
37+
RUN mkdir -p /app-output/public /app-output/.next/static && \
38+
cp -r .next/standalone/. /app-output/ && \
39+
cp -r .next/static/. /app-output/.next/static/ && \
40+
cp -r public/. /app-output/public/ && \
41+
chmod -R g=u /app-output && \
42+
chgrp -R 0 /app-output
43+
44+
# Production image — Red Hat Hardened Image (distroless, ~48MB, 0 CVEs)
45+
FROM registry.access.redhat.com/hi/nodejs:latest AS runner
46+
47+
ARG GIT_COMMIT=unknown
48+
49+
WORKDIR /app
50+
51+
ENV NODE_ENV=production
52+
ENV NEXT_TELEMETRY_DISABLED=1
53+
54+
COPY --from=builder /app-output/. ./
55+
56+
USER 1001
57+
58+
EXPOSE 3000
59+
60+
ENV PORT=3000
61+
ENV HOSTNAME="0.0.0.0"
62+
63+
# server.js is created by next build from the standalone output
64+
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
65+
LABEL org.opencontainers.image.revision=$GIT_COMMIT
66+
67+
CMD ["node", "server.js"]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema.json",
3+
"style": "new-york",
4+
"rsc": true,
5+
"tsx": true,
6+
"tailwind": {
7+
"config": "",
8+
"css": "src/app/globals.css",
9+
"baseColor": "neutral",
10+
"cssVariables": true,
11+
"prefix": ""
12+
},
13+
"iconLibrary": "lucide",
14+
"aliases": {
15+
"components": "@/components",
16+
"utils": "@/lib/utils",
17+
"ui": "@/components/ui",
18+
"lib": "@/lib",
19+
"hooks": "@/hooks"
20+
},
21+
"registries": {}
22+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { dirname } from "path";
2+
import { fileURLToPath } from "url";
3+
import { FlatCompat } from "@eslint/eslintrc";
4+
5+
const __filename = fileURLToPath(import.meta.url);
6+
const __dirname = dirname(__filename);
7+
8+
const compat = new FlatCompat({
9+
baseDirectory: __dirname,
10+
});
11+
12+
const eslintConfig = [
13+
...compat.extends("next/core-web-vitals", "next/typescript"),
14+
{
15+
ignores: [
16+
"node_modules/**",
17+
".next/**",
18+
"out/**",
19+
"build/**",
20+
"next-env.d.ts",
21+
],
22+
},
23+
{
24+
rules: {
25+
"@typescript-eslint/no-explicit-any": "error",
26+
"@typescript-eslint/no-unused-vars": "error",
27+
"react-hooks/exhaustive-deps": "warn",
28+
},
29+
},
30+
];
31+
32+
export default eslintConfig;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// eslint-disable-next-line @typescript-eslint/no-require-imports
2+
const path = require('path')
3+
4+
/** @type {import('next').NextConfig} */
5+
const nextConfig = {
6+
output: 'standalone',
7+
outputFileTracingRoot: path.resolve(__dirname, '../..'),
8+
transpilePackages: ['ambient-sdk'],
9+
experimental: {
10+
staticGenerationMinPagesPerWorker: 100,
11+
},
12+
}
13+
14+
module.exports = nextConfig

0 commit comments

Comments
 (0)