---
name: "GMAL Scope Builder"
client: Oliver Agency
status: active
tech: [React, TypeScript, FastAPI, PostgreSQL, Claude Opus, Docker, Apache]
local_path: /Users/ai_leed/Documents/Projects/Oliver/gmal-scope-builder
deploy: sudo ./deploy.sh
url: https://optical-dev.oliver.solutions/gsb/
server: optical-dev
tags: [oliver, gmal, ratecard, fte, ai, claude, postgres, azure-sso]
created: 2026-04-14
last_commit: 2026-03-31
commits: 23
port: 8002
db: PostgreSQL 16
---

## Overview

GMAL Scope Builder is an internal Oliver Agency tool for scoping new client ratecards against the Global Master Asset Library (GMAL) database. It enables teams to parse client briefs, match deliverables to standardised GMAL assets using Claude Opus 4.6, compute hours per role, and generate Excel/PDF exports for proposal creation. The system combines AI-powered matching with manual refinement, efficiency profiling, and team shape calculation to produce accurate scope documents.

## Tech Stack

- **Frontend:** React 18, TypeScript, Vite, Axios, MSAL (Azure AD auth)
- **Backend:** FastAPI, Python 3.12, SQLAlchemy async (asyncpg), Pydantic
- **Database:** PostgreSQL 16 (Alpine)
- **Infrastructure:** Docker Compose (3 containers: db, backend, frontend as static), Apache 2 reverse proxy
- **AI/ML:** Anthropic Claude Opus 4.6 (matching, descriptions, refinement)
- **Key libraries:** openpyxl (Excel parsing), python-pptx (doc extraction), Playwright (e2e tests)

## Architecture

GMAL Scope Builder follows a classic three-tier architecture with AI as a supplementary service tier.

```
┌─────────────────────────────────────────────────────────────┐
│ Browser (React SPA)                                         │
│ - Auth: Azure AD MSAL token                                │
│ - Routes: Dashboard, ProjectView, GmalBrowser, Ratecard   │
└──────────────────────┬──────────────────────────────────────┘
                       │ Axios → /gsb/api/* with Bearer token
                       ▼
┌─────────────────────────────────────────────────────────────┐
│ Apache 2 (optical-dev.oliver.solutions)                     │
│ - ProxyPass /gsb/api/ → 127.0.0.1:8002                     │
│ - Alias /gsb → /var/www/html/gmal-scope-builder (static)   │
└──────────────────────┬──────────────────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────────────────┐
│ FastAPI (Uvicorn, port 8002 / Docker 8000)                 │
│ - JWT validation (Azure AD JWKS cache)                     │
│ - Role-based access control (Viewer/Editor/Admin)          │
│ - Endpoints: /api/gmal/*, /api/projects/*, /api/matching/  │
│              /api/ratecard/*, /api/efficiency/*, /api/users/│
└──────────────────────┬──────────────────────────────────────┘
                       │
        ┌──────────────┼──────────────┐
        ▼              ▼              ▼
    PostgreSQL    Claude Opus    openpyxl/
    (port 5433)   (HTTPS API)    file parsing
```

**Key Components:**

- **GMAL Ingestion:** `excel_parser.py` reads standardised GMAL workbook, populates `GmalAsset`, `Role`, `GmalHours`, `ServiceLine` tables.
- **Brief Analysis:** `doc_parser.py` extracts text from PDF/PPTX, Claude extracts structured asset list via tool call.
- **AI Matching:** `ai_matching.py` sends client assets + GMAL catalog to Claude, receives match suggestions (batched, cancellable).
- **Ratecard Build:** `ratecard_builder.py` looks up `GmalHours` for each matched asset per role, calculates delivery hours, applies efficiency curves, derives team FTE.
- **Match Refinement:** `match_refiner.py` allows users to refine matches via natural-language chat (e.g., "use Senior Producer instead of Producer").
- **Role-Based Access:** Middleware validates Azure AD token, middleware/auth.py checks RBAC on sensitive endpoints.

**Critical Invariant:** `model_type` (scope complexity tier: Small/Medium/Large) is immutable after Project creation—all GmalHours lookups depend on it.

## Dev Commands

```bash
# Clone
git clone git@bitbucket.org:zlalani/gmal-scope-builder.git
cd gmal-scope-builder

# Local dev (requires .env with ANTHROPIC_API_KEY, AZURE_TENANT_ID, AZURE_CLIENT_ID)
cp .env.example .env
# Edit .env: fill secrets, set DEV_AUTH_BYPASS=1 to skip JWT locally

# Start all services
docker compose up -d

# Backend logs
docker compose logs -f backend

# Frontend dev (separate terminal)
cd frontend
npm install
npm run dev  # Vite dev server (http://localhost:5173)

# Run e2e tests
npm install -g @playwright/test
cd e2e
npx playwright test

# Trigger GMAL ingest (if JWT validation enabled)
curl -X POST -H "Authorization: Bearer <token>" \
  http://localhost:8002/api/gmal/ingest

# DB access
docker compose exec db psql -U scope_user -d scope_builder

# Stop
docker compose down
```

## Deployment

- **Server:** optical-dev (optical-dev.oliver.solutions)
- **Deploy Command:** `sudo ./deploy.sh` (idempotent, pulls from main, rebuilds Docker, builds frontend, reloads Apache)
- **URL:** https://optical-dev.oliver.solutions/gsb/
- **Port:** 8002 (backend, bound to 127.0.0.1 only; frontend served as static by Apache)
- **Service:** Not a systemd service; managed by Docker Compose + Apache
- **Local path:** /Users/ai_leed/Documents/Projects/Oliver/gmal-scope-builder
- **Server path:** /opt/gmal-scope-builder (or clone destination)
- **Static files:** /var/www/html/gmal-scope-builder/
- **Data dir:** /var/www/html/gmal-scope-builder/data/ (mounted into db/backend)

**Deployment Process:**
1. Pre-flight: check .env, Docker, required secrets
2. `git pull origin main`
3. `docker compose up -d --build --remove-orphans`
4. Wait for `/api/health` (90s timeout)
5. Build frontend: `docker run node:20-alpine npm ci && npm run build` → `/var/www/html/gmal-scope-builder/`
6. Add Apache ProxyPass/Alias blocks (idempotent check for marker)
7. `apache2ctl configtest && systemctl reload apache2`

Typical deploy time: 2–4 minutes.

## Environment Variables

- `POSTGRES_PASSWORD` — PostgreSQL superuser password; used in `docker-compose.yml` to set `DATABASE_URL`
- `ANTHROPIC_API_KEY` — Anthropic API key (sk-ant-api03-…); required for Claude Opus calls
- `AZURE_TENANT_ID` — Azure AD tenant ID; for MSAL (frontend) and JWKS (backend)
- `AZURE_CLIENT_ID` — Azure AD app registration ID; for MSAL and JWKS
- `DEV_AUTH_BYPASS` — Set to `1` to skip JWT validation (local dev only); unset in production
- `DATA_DIR` — Override GMAL data directory (default: `./data`); Docker-mounted to both backend and frontend
- `DATABASE_URL` — Auto-generated from POSTGRES_PASSWORD; async psycopg3 connection string
- `DATABASE_URL_SYNC` — Auto-generated; sync version for openpyxl ingest (blocking)

## API Endpoints

| Endpoint | Method | Purpose |
|----------|--------|---------|
| `/api/health` | GET | Health check (no auth) |
| `/api/gmal/stats` | GET | GMAL asset counts |
| `/api/gmal/ingest` | POST | Trigger GMAL Excel ingest (admin only) |
| `/api/gmal/browse` | GET | List/search GMAL assets (query params: `q`, `service_line`, `role`) |
| `/api/gmal/describe/{asset_id}` | GET | Get asset details + AI description |
| `/api/projects` | GET/POST | List / create project |
| `/api/projects/{id}` | GET/PATCH/DELETE | Project CRUD |
| `/api/projects/{id}/upload` | POST | Upload client brief (PDF/PPTX); multipart form |
| `/api/projects/{id}/analyze` | GET | Extract brief structure (scope, risks, assumptions) |
| `/api/projects/{id}/matching/extract` | POST | Extract client assets from brief + run matching (normal/deep modes) |
| `/api/projects/{id}/matching/select` | POST | Lock user's match selections; send to ratecard build |
| `/api/projects/{id}/matching/refine` | POST | Refine matches via chat (e.g., "use Senior instead") |
| `/api/projects/{id}/ratecard/build` | GET | Build ratecard from selected matches + efficiency |
| `/api/projects/{id}/ratecard/export` | POST | Export ratecard as Excel or PDF |
| `/api/projects/{id}/ratecard/team-shape` | GET | Calculate FTE per role (hours / 1800) |
| `/api/efficiency/*` | GET/POST/PATCH | Efficiency profiles, tool efficiencies |
| `/api/users/me` | GET | Current user info |
| `/api/users` | GET | List users (admin only) |
| `/api/users/{id}/role` | PATCH | Update

## Timeline / Git History
| Date | Change |
|------|--------|
| 2026-03-31 | Add GMAL standard caveats to ratecard Excel export |
| 2026-03-30 | Fix 401: send ID token instead of Graph access token |
| 2026-03-30 | Fix logout for MSAL v5: use clearCache() |
| 2026-03-30 | Fix 504 timeouts on upload/match + broken exports |
| 2026-03-28 | Add Azure SSO + production deployment config |
| 2026-03-28 | Add deploy.sh + fix TS build errors |
| 2026-03-28 | Add DEV_AUTH_BYPASS for local dev |
| 2026-03-28 | Increase Vite proxy timeout to 5min for long AI calls |

## Sessions
### 2026-04-14 – Project catalogued
**Done:** Added to Obsidian second brain with full details.

---
## Change Log
| Date | Requested | Changed | Files |
|------|-----------|---------|-------|
| 2026-03-31 | Add caveats to export | GMAL standard caveats row in Excel | export.py |
| 2026-03-28 | Add Azure SSO | MSAL v5 auth + production config | frontend, backend |

## Related
- [[build-a-squad/Build A Squad]] (same FTE scoping concept)
- [[ac-helper/AC Helper]]