# AGENTS.md

This file provides guidance to Claude Code (claude.ai/code) and other agents when working with code in this repository.

## Overview

**Primary Purpose: Pull Request Instance Support**

This nginx-based reverse proxy service's main focus is providing dynamic PR instance routing for testing frontend pull requests before merge. It enables developers to test any frontend PR on a unique, shareable URL without modifying the proxy configuration.

PR instances are accessed at: `{app-shortcode}-{prID}.pullrequests.{brand}.com`

Secondary purpose: The proxy also serves 30+ standalone React applications for QA testing on dedicated domains.

The service is deployed to AWS Fargate and serves applications on `theorchard.io` and brand-specific domains.

## Common Commands

### Local Development

```bash
# Build and run the Docker container
docker build -t standalone-frontends .
docker run -td standalone-frontends

# Run the full stack (nginx proxy + backend + redis)
docker-compose up -d

# View nginx logs
docker-compose logs -f standalone-frontends

# Reload nginx configuration (after changes)
docker exec <container_id> nginx -s reload

# Test nginx configuration syntax
docker exec <container_id> nginx -t
```

### CI/CD

The Jenkinsfile automatically builds and deploys on commits to master. Manual retrigger via comment: "retest this please"

## Architecture

### Pull Request Instance System (Primary Feature)

**URL Pattern**: `{app-shortcode}-{prID}.pullrequests.{brand}.com`

Examples:
- `podcast-123.pullrequests.qaorch.com` → Podcast frontend PR #123 (Orch brand)
- `seat-456.pullrequests.qapdesuite.com` → Seat frontend PR #456 (PDE Suite brand)
- `workstation-789.pullrequests.qaawal.com` → Workstation frontend PR #789 (Awal brand)

**Supported Brands** (4 total):
- `qaorch.com` - Orchard (no index suffix)
- `qapdesuite.com` - PDE Suite (uses `-sme` suffix)
- `qaawal.com` - Awal (uses `-awal` suffix)
- `qakollectivenr.com` - Kollective (uses `-kollectivenr` suffix)

**How It Works**:

1. **Wildcard Server Blocks** (nginx.conf lines 828-877)
   - 4 server blocks, one per brand: `*.pullrequests.{brand}.com`
   - All requests proxy to `$load_pr_instance` (NJS variable)
   - No per-app configuration needed

2. **Dynamic Routing via NJS** (`js/auth0Redirect.js:45-85`)
   - `load_pr_instance()` function parses the subdomain
   - Extracts app shortcode and PR number
   - Looks up full app path from `appPathMap`
   - Determines brand suffix from domain
   - Returns CDN URL: `https://qa-cdn.theorchard.io/{appPath}/prs/{prID}/index{suffix}.html`

3. **PR Hub Pages** (nginx.conf lines 879-945)
   - Landing pages at `pullrequests.{brand}.com`
   - Serves `html/pullrequests_index.html` template
   - Provides `/auth0Redirect` endpoint for OAuth flows

4. **App Path Mapping** (`js/auth0Redirect.js:3-22`)
   - 22 app shortcodes mapped to full CDN paths
   - Examples: `podcast` → `frontend-podcast`, `seat` → `frontend-seat`
   - Fallback: Handles full `frontend-*` names in subdomain (lines 52-53)

### Static Application Servers (Secondary Feature)

- 30+ dedicated server blocks (nginx.conf lines 31-825)
- Pattern: `qa-frontend-{app-name}-standalone.theorchard.io`
- Routes `/grass/` or `/api/` → backend, `/index.html` → CDN
- SPA routing: `try_files /fake_file.html /index.html`

### Key Components

- **nginx.conf**: Main configuration with 30+ server blocks for individual apps
- **js/auth0Redirect.js**: NJS module providing:
  - `auth0_redirect()`: Handles Auth0 OAuth callback redirects
  - `load_pr_instance()`: Routes PR instances to correct CDN paths
- **entry_point.sh**: Dynamically injects DNS resolver at container startup
- **docker-compose.yml**: Local dev stack with 3 services:
  - `standalone-frontends`: The nginx proxy (port 80)
  - `redis`: Cache for backend (port 6379)
  - `ows-grass`: Backend API service (port 8080)

### Service Dependencies

```
Browser → Nginx Proxy → QA CDN (React bundles)
                     ↓
                  OWS Grass Backend → Redis Cache
```

## Adding a New Pull Request Application

**This is the most common task** - adding PR instance support for a new frontend.

### Steps:

1. **Add app shortcode to `appPathMap`** in `js/auth0Redirect.js`:
```javascript
const appPathMap = {
  // ... existing entries
  mynewapp: 'frontend-mynewapp',  // shortcode: CDN path
};
```

2. **Ensure frontend CI/CD deploys to correct CDN structure**:
   - Main branch: `qa-cdn.theorchard.io/frontend-mynewapp/main-index.html`
   - PR instances: `qa-cdn.theorchard.io/frontend-mynewapp/prs/{prID}/index.html`
   - Brand variants (if needed):
     - `index.html` (Orchard - default)
     - `index-sme.html` (PDE Suite)
     - `index-awal.html` (Awal)
     - `index-kollectivenr.html` (Kollective)

3. **Test the PR instance**:
   - URL pattern: `mynewapp-{prID}.pullrequests.qaorch.com`
   - Example: `mynewapp-42.pullrequests.qaorch.com`

**That's it!** No nginx.conf changes needed - the wildcard server blocks handle all apps automatically.

### Naming Conventions:

- **App shortcode**: Short name used in subdomain (e.g., `podcast`, `seat`, `workstation`)
- **CDN path**: Full frontend name (e.g., `frontend-podcast`, `frontend-seat`)
- **Fallback**: If you use the full name in subdomain (e.g., `frontend-seat-42`), it works without appPathMap entry

### Brand Index File Suffix Logic:

```javascript
// From auth0Redirect.js:56-67
switch (brandDomain) {
  case 'orch':
    brandIndexSuffix = '';           // index.html
    break;
  case 'pdesuite':
    brandIndexSuffix = '-sme';       // index-sme.html
    break;
  default:
    brandIndexSuffix = `-${brandDomain}`;  // index-{brand}.html
}
```

## Internal HTTPS-Hosted PR Applications

Some applications serve assets from internal HTTPS endpoints instead of the public CDN.

### Architecture

Each application has its own base HTTPS URL, then uses standard path structure.

Examples:
- seat: `https://seat.qapdesuite.com/frontend-seat/prs/123/index.html`
- insights: `https://insights.qaorch.com/frontend-insights/prs/456/index.html`

The NJS routing module maps app shortcodes to base URLs and constructs the full path.

### Adding an Internal HTTPS-Hosted Application

1. **Ensure assets are deployed** to the app's HTTPS endpoint:
   - Pattern: `{baseUrl}/{repo}/prs/{prID}/`
   - Example for seat (Orchard brand): `https://seat.qapdesuite.com/frontend-seat/prs/123/index.html`
   - Example for seat (PDE Suite brand): `https://seat.qapdesuite.com/frontend-seat/prs/123/index-sme.html`
   - Must include all brand variant index files and referenced bundles (same variants as CDN-backed apps)

2. **Add app mapping to `internalHostedApps`** in `js/auth0Redirect.js`:
   ```javascript
   const internalHostedApps = {
     seat: 'https://seat.qapdesuite.com',
     insights: 'https://insights.qaorch.com',
     newapp: 'https://newapp.yourbrand.com',  // Add your app here
   };
   ```

3. **Ensure app is in `appPathMap`** (usually already there):
   ```javascript
   const appPathMap = {
     newapp: 'frontend-newapp',
   };
   ```

4. **Test the PR instance:**
   - URL: `newapp-{prID}.pullrequests.{brand}.com`
   - Example: `newapp-42.pullrequests.qaorch.com`

**No other changes needed** - nginx wildcard server blocks handle all routing automatically.

### Difference from CDN-Backed Apps

- **CDN apps:** Assets from `qa-cdn.theorchard.io`
- **Internal apps:** Assets from app-specific HTTPS endpoints (e.g., `seat.qapdesuite.com`)
- Both use the same PR instance URL pattern: `{app}-{prID}.pullrequests.{brand}.com`
- Both resolve the same brand-specific index filename (e.g., `index-sme.html` for PDE Suite)

## Adding a Static Standalone Application (Less Common)

Only needed if you want a permanent QA domain separate from PR instances:

1. Add server block to nginx.conf (follow pattern at lines 31-825)
2. Add DNS CNAME: `qa-frontend-{app-name}-standalone.theorchard.io`
3. Deploy React bundle to `qa-cdn.theorchard.io/{app-path}/main-index.html`

## PR Instance Routing Deep Dive

### NJS Module: `js/auth0Redirect.js`

This file exports two functions used as nginx variables:

#### 1. `load_pr_instance()` - The Core PR Router

**Input**: HTTP request with Host header like `podcast-123.pullrequests.qaorch.com`

**Algorithm** (lines 46-85):
```javascript
function parsePrPath(hostName, req) {
  // 1. Parse subdomain
  const subDomain = hostName.split('.')[0];  // "podcast-123"

  // 2. Extract PR ID (last segment after dash)
  const prID = subDomain.split('-').slice(-1)[0];  // "123"

  // 3. Extract app shortcode (everything before last dash)
  const shortcode = subDomain.split('-')[0];  // "podcast"

  // 4. Look up full CDN path
  let appPath = appPathMap[shortcode];  // "frontend-podcast"

  // 5. Fallback: Handle full "frontend-*" names
  if (!appPath && subDomain.includes('frontend-'))
    appPath = subDomain.replace(`-${prID}`, '');  // "frontend-podcast"

  // 6. Determine brand suffix
  const brandDomain = hostName.split('.').slice(-2, -1)[0].replace('qa', '');
  // "orch" → "", "pdesuite" → "-sme", "awal" → "-awal"

  // 7. Construct CDN URL
  return `https://qa-cdn.theorchard.io/${appPath}/prs/${prID}/index${brandIndexSuffix}.html`;
}
```

**Output**: CDN URL to proxy to, e.g.:
- `https://qa-cdn.theorchard.io/frontend-podcast/prs/123/index.html`

**Error Handling**: Returns 200 with error message if app not found in map

#### 2. `auth0_redirect()` - OAuth Callback Handler

**Purpose**: Redirects Auth0 login callbacks to the original PR instance URL

**Input**: Request to `/auth0Redirect` with query params from Auth0

**Algorithm** (lines 26-43):
```javascript
// Parse query string: ?to=podcast-123.pullrequests.qaorch.com&code=...&state=...
const parsedQueryParamsObj = qs.parse(queryParams);
const prInstanceHost = parsedQueryParamsObj.to;

// Preserve Auth0 params (code, state) and redirect
const proxyHost = `https://${prInstanceHost}?${queryParams.replace(`to=${prInstanceHost}&`, '')}`;
return proxyHost;  // "https://podcast-123.pullrequests.qaorch.com?code=...&state=..."
```

### App Path Map

Defined in `js/auth0Redirect.js:3-22` as `appPathMap` object.

Maps shortcodes to CDN paths (examples):
- `podcast` → `frontend-podcast`
- `seat` → `frontend-seat`
- `abacus` → `frontend-royalties`

Check the file for the complete list of supported applications.

### PR Hub Index Pages

**Purpose**: Landing pages at `pullrequests.{brand}.com` (4 brands)

**Features**:
- Serves `html/pullrequests_index.html` template
- Provides `/auth0Redirect` endpoint for OAuth callback handling
- Currently minimal template (lines 1-15) - could be enhanced with PR directory listing

## Configuration Notes

- **DNS Resolution**: Uses dynamic resolver injected at runtime via `entry_point.sh`
- **CORS Headers**: Most server blocks add `Access-Control-Allow-Origin: *` for API routes
- **Referer Validation**: Some sensitive endpoints validate referer against allowed domains
- **Backend Variable**: `$grass_backend` typically set to `https://qa-ows-grass.theorchard.io:443`
- **SPA Pattern**: All apps use `try_files /fake_file.html /index.html` to support client-side routing

## Deployment

- **Target**: AWS Fargate (QA environment)
- **CI/CD**: Jenkins pipeline with stages for SAST, Docker build/scan, and deployment
- **Monitoring**: Datadog integration via software-catalog.yaml
- **Ownership**: @theorchard/devops-squad (see CODEOWNERS)
