Jennah UI is a React-based cloud workload management dashboard for the Project Jennah distributed backend system. It provides a user-friendly interface for submitting compute jobs, monitoring job execution status, and managing tenant workloads through a secure, authenticated web interface deployed on Google Cloud Run.
| Layer | Technology |
|---|---|
| Framework | React 19 + TypeScript (Vite) |
| API Transport | ConnectRPC (@connectrpc/connect-web v2) |
| Protobuf Codegen | Buf CLI + @bufbuild/protobuf v2 |
| Styling | Tailwind CSS v4 + shadcn/ui components |
| Icons | MUI Icons + Lucide React |
| Routing | React Router v7 |
| Containerization | Docker (multi-stage build) |
| Web Server | Nginx (SPA routing + API proxy) |
| Deployment | Google Cloud Run |
| Authentication | oauth2-proxy (GitHub OAuth) as a sidecar container |
User Browser
│
▼
oauth2-proxy :8080 (GitHub OAuth sidecar — validates session, forwards headers)
│
▼
Nginx :3000 (serves SPA, proxies /api/* to Gateway)
│
├── /* ──────────► React SPA (static files)
│
└── /api/* ──────► jennah-gateway (Cloud Run)
│
▼
Worker Nodes → GCP Batch → Cloud Spanner
The Nginx configuration forwards X-Forwarded-Email and X-Forwarded-User headers from the proxy to the Gateway as X-OAuth-Email and X-OAuth-UserId.
- Job submission with GCP-compliant name validation
- Real-time job status dashboard (PENDING → SCHEDULED → RUNNING → COMPLETED/FAILED)
- Cancel and delete jobs
- View full job details (container image, machine type, env vars, created time)
- GCP Batch Console ID display (
job-name-xxxxxxxxformat) - GitHub OAuth authentication with oauth2-proxy sidecar
- Tenant-scoped job listing (jobs filtered by authenticated tenant)
- Local development mode with mock auth bypass
jennah-ui/
├── proto/
│ └── jennah.proto # Source-of-truth protobuf definition
├── src/
│ ├── api/
│ │ ├── auth.ts # OAuth2-proxy user info + DEV mock
│ │ ├── client.ts # ConnectRPC client (with DEV auth interceptor)
│ │ ├── transport.ts # ConnectRPC transport layer
│ │ └── hooks/
│ │ ├── useSubmitJob.ts
│ │ ├── useListJobs.ts
│ │ ├── useGetJob.ts
│ │ ├── useGetCurrentTenant.ts
│ │ ├── useCancelJob.ts
│ │ └── useDeleteJob.ts
│ ├── gen/proto/ # Auto-generated — do not edit by hand
│ │ ├── jennah_pb.ts # Message types
│ │ └── jennah_connect.ts # Service client
│ ├── components/ # Reusable UI components
│ ├── pages/ # Route-level page components
│ ├── context/ # AuthContext
│ └── App.tsx # Router + protected routes
├── buf.gen.yaml # Buf codegen configuration
├── vite.config.ts # Vite + dev proxy config
├── nginx.conf # Nginx SPA + proxy config
├── Dockerfile # Multi-stage build
├── service.yaml # Cloud Run manifest template
└── service.resolved.yaml # Resolved manifest (never commit)
- Node.js 20+
- npm 10+
- Buf CLI (for protobuf codegen)
npm installCreate a .env file at the project root:
# Dev mock auth — bypasses oauth2-proxy locally
VITE_DEV_EMAIL=you@example.com
VITE_DEV_USER_ID=dev-user-123
# API gateway (leave as /api — Vite proxy handles routing locally)
VITE_API_GATEWAY_URL=/api
# OAuth + deployment (required for service.yaml substitution only)
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...
COOKIE_SECRET=...
REDIRECT_URL=https://YOUR_SERVICE_URL/oauth2/callbackIn
DEVmode (npm run dev), the app automatically usesVITE_DEV_EMAILandVITE_DEV_USER_IDinstead of calling the oauth2-proxy/oauth2/userinfoendpoint. The Vite dev proxy forwards all/apirequests to the live Gateway.
npx buf generateGenerated files are written to src/gen/proto/. Never edit them manually.
All API calls use ConnectRPC typed hooks located in src/api/hooks/.
The client injects X-OAuth-Email, X-OAuth-UserId, and X-OAuth-Provider headers automatically
in DEV mode via an interceptor in src/api/client.ts.
| Hook | RPC | Description |
|---|---|---|
useSubmitJob |
SubmitJob |
Create and dispatch a new job |
useListJobs |
ListJobs |
Fetch all jobs for the current tenant |
useGetJob |
GetJob |
Fetch full details for a single job by ID |
useGetCurrentTenant |
GetCurrentTenant |
Resolve calling tenant from auth headers |
useCancelJob |
CancelJob |
Stop a running/pending job (data preserved) |
useDeleteJob |
DeleteJob |
Remove a job from GCP Batch and the database |
Google Cloud Batch enforces strict naming rules. The job name input automatically:
- Converts to lowercase
- Replaces spaces with hyphens
- Strips all non-alphanumeric/hyphen characters
- Enforces a 54-character maximum (GCP limit is 63; Gateway appends
-xxxxxxxx)
The app uses a multi-stage Docker build:
- Stage 1 (
node:20-alpine): Installs dependencies and compiles TypeScript →dist/ - Stage 2 (
nginx:alpine): Copiesdist/into Nginx and appliesnginx.conf
Nginx listens on 127.0.0.1:3000 and:
- Serves SPA assets with
try_files $uri /index.htmlfallback - Proxies
/api/*to the Gateway with auth headers forwarded
Build the container:
docker build -t asia-docker.pkg.dev/labs-169405/asia.gcr.io/jennah-ui:vN .Push to Artifact Registry:
docker push asia-docker.pkg.dev/labs-169405/asia.gcr.io/jennah-ui:vNset -a; source .env; set +a
envsubst < service.yaml > service.resolved.yamlOr edit service.resolved.yaml directly to bump the image version tag.
gcloud run services replace service.resolved.yaml --region asia-northeast1| Service | URL |
|---|---|
| Frontend | https://jennah-ui-382915581671.asia-northeast1.run.app |
| Gateway | https://jennah-gateway-382915581671.asia-northeast1.run.app |
oauth2-proxy runs as a sidecar container alongside the UI:
Browser → oauth2-proxy :8080
│ valid session?
├── No → redirect to https://github.com/login/oauth/authorize
└── Yes → forward request to localhost:3000 with headers:
X-Forwarded-Email: user@example.com
X-Forwarded-User: github-user-id
Nginx passes these headers downstream to the Gateway as X-OAuth-Email / X-OAuth-UserId.
The Gateway resolves the calling tenant from those headers — no tenant ID is passed in request bodies.
OAuth callback URL:
https://jennah-ui-382915581671.asia-northeast1.run.app/oauth2/callback
Routes that skip authentication:
^/auth/^/oauth2/^/assets/GET ^/$
- HTTPS-only cookies enforced (
--cookie-secure=true) - CSRF protection enabled (
--cookie-csrf-per-request=true) - OAuth credentials and cookie secrets never stored in source code
service.resolved.yamlis git-ignored- Job names are sanitized client-side before submission to prevent GCP Batch name rejection
| Task | Action |
|---|---|
| Deploy UI changes | Bump version tag in service.resolved.yaml, build + push image, run gcloud run services replace |
| Proto changes | Update proto/jennah.proto, run npx buf generate, update affected hooks/pages |
| Rotate OAuth secret | Update .env, re-resolve manifest, redeploy |
| View logs | Google Cloud Console → Cloud Run → jennah-ui → Logs |
| View jobs in GCP | GCP Console → Batch → Jobs → filter by {job.name}-{jobId[:8]} |