# utilisation-dept L'Oréal Utilisation Dashboard — internal tool that merges Zoho time-log exports with Airtable resource/booking data and renders utilisation charts per department, per person, and per project. This is a clean rewrite of an earlier static SPA that shipped an Airtable Personal Access Token in the JS bundle. The new architecture keeps all secrets on the backend. ## Architecture ``` Browser ─► Apache (optical-dev.oliver.solutions, shared vhost) ├─ /utilisation-dept/ → static SPA (Vite build in /var/www/html/utilisation-dept/) └─ /utilisation-dept/api/ → FastAPI container (127.0.0.1:) │ └─ Airtable REST API (PAT held in .env) ``` - **Backend**: FastAPI + uvicorn in a Docker container, bound to 127.0.0.1 only. Apache fronts the public traffic. - **Frontend**: React + Vite + TypeScript + Tailwind + Recharts. Vite `base: '/utilisation-dept/'`. Built into `/var/www/html/utilisation-dept/`. - **Database**: none. Airtable is the source of truth; uploaded Zoho files are parsed in memory and discarded. - **Auth**: local admin account today (bcrypt creds in `.env`). Designed for an Azure AD/MSAL swap-in later — see `app/auth/azure.py`. ## First-time setup ### 1. Rotate the Airtable PAT (do this before anything else) The old SPA had its PAT hardcoded in the JS bundle. Assume that token is compromised. 1. Go to . 2. Revoke the old token (`patHAB...`) if it still exists. 3. Create a new Personal Access Token: - **Name**: `utilisation-dept-backend` - **Scopes**: `data.records:read` - **Access**: limit to base `appoByydxIQANKtSh` only, tables `Resource` and `Booking Resource`. 4. Copy the token. You'll paste it into `.env` below. ### 2. Generate secrets ```bash # Session secret (paste output into SESSION_SECRET): python3 -c "import secrets; print(secrets.token_urlsafe(48))" # Admin password hash (replace 'your-password' with the real password): python3 -c "from passlib.hash import bcrypt; print(bcrypt.hash('your-password'))" ``` > **Heads-up**: bcrypt hashes contain `$` characters. Wrap the hash in single > quotes in `.env`, otherwise docker-compose will treat the `$...` segments as > variable substitutions and the container will receive a truncated value: > > ``` > ADMIN_PASSWORD_BCRYPT='$2b$12$abc...xyz' > ``` ### 3. Configure `.env` ```bash cp .env.example .env # edit .env and fill in: # AIRTABLE_PAT (from step 1) # SESSION_SECRET (from step 2) # ADMIN_PASSWORD_BCRYPT (from step 2) ``` ## Local development ```bash # Backend cd backend python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt uvicorn app.main:app --reload --port 8200 # Frontend (in another terminal) cd frontend npm install npm run dev # open http://localhost:5173/utilisation-dept/ ``` The Vite dev server proxies `/utilisation-dept/api/*` to `http://localhost:8200`. To skip auth entirely during dev, set `DEV_AUTH_BYPASS=true` in `.env` (NEVER do this in production — the startup banner will warn loudly). ## Local Docker ```bash docker compose up --build # health: curl http://localhost:8200/api/health ``` ## Tests ```bash cd backend && pytest cd frontend && npm run typecheck && npm run lint ``` ## Deployment to optical-dev.oliver.solutions The server hosts many small apps behind a shared Apache vhost. This app lives at `/opt/utilisation-dept/`. ### First deploy ```bash ssh user@optical-dev.oliver.solutions sudo git clone /opt/utilisation-dept cd /opt/utilisation-dept # Create .env (see "First-time setup" above for value generation). sudo cp .env.example .env sudo vi .env ./deploy/deploy.sh ``` `deploy.sh` will: - Pick a free port in 8200–8299 and persist it to `.env`. - Render `deploy/apache-utilisation-dept.conf` from the template. - Build the frontend → `/var/www/html/utilisation-dept/`. - Build + start the Docker container. - Health-poll `/api/health`. - Print the Apache `Include` line for the shared vhost. ### Wire into the shared vhost (one time only) Add **inside** `` of `/etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf`: ```apache Include /opt/utilisation-dept/deploy/apache-utilisation-dept.conf ``` Then: ```bash sudo apachectl configtest && sudo systemctl reload apache2 ``` ### Smoke-check ```bash curl -sI https://optical-dev.oliver.solutions/utilisation-dept/api/health curl -sf https://optical-dev.oliver.solutions/utilisation-dept/api/health # expect 200 and {"ok":true,"version":"0.1.0"} ``` Then open in a browser and confirm: - Login screen renders. - View-source on the loaded JS: search for `patHAB` — must return nothing. - Logging in with the local admin creds shows the dashboard. ### Subsequent deploys ```bash ssh user@optical-dev.oliver.solutions cd /opt/utilisation-dept ./deploy/deploy.sh # --no-pull, --no-build, --no-frontend, --logs available ``` ## Operations ### Where the auth log lives `/opt/utilisation-dept/backend/logs/auth.log` (mounted into the container at `/app/logs/`). Rotating, 5 MB × 5 backups. This is the only audit trail for who logged in when — there's no database. ### Rotating the PAT 1. Generate a new PAT (same scopes as in setup). 2. Edit `.env` → update `AIRTABLE_PAT`. 3. `docker compose -p utilisation-dept restart backend` 4. Revoke the old PAT in the Airtable portal. ### Forgot admin password 1. Generate a new bcrypt hash: `python3 -c "from passlib.hash import bcrypt; print(bcrypt.hash('new-password'))"` 2. Edit `.env` → update `ADMIN_PASSWORD_BCRYPT`. 3. `docker compose -p utilisation-dept restart backend` ## Future work - **Azure AD / MSAL SSO**: `app/auth/azure.py` is stubbed. Flip `AUTH_MODE=azure` in `.env`, fill in `AZURE_TENANT_ID` + `AZURE_CLIENT_ID`, register the SPA in Azure (platform type: **Single-page application**), wire MSAL on the frontend, send the **ID token** to the backend. See `~/.claude/skills/azure-ad-msal-auth.md`. - **Database** if/when we need history, audit trails, or to cache more aggressively. Today the data lives in Airtable and the Zoho upload is ephemeral.