Three big pages rebuilt against the original's screen recording — addresses the "the chart is not dynamic" + "see booking and timesheet together" feedback. 72/72 backend tests (+12 new), frontend typecheck/lint/build clean, main entry 16.43 KB gzipped. Department page: - Department pill subnav (Creative / PM Team / Syndication / Transcreation / Opera Upload / Operations / Adhoc People). Replaces dropdown. - Last Week / This Week / Next Week pills with date-range labels + month-preset pills derived from the uploaded timelog. - HOURS & UTILISATION strip: 6 tiles — Allocated (net of leave), Time Logged (net of leave), Active People, Actual %, Forecast % (amber highlight), Leave Hours. - BILLABILITY BREAKDOWN strip: Fee Related, Client Related, Non-Billable, Total Billable %. - FTE VS FREELANCER section: two large composite cards, each with nested Actual + Forecast utilisation sub-cards. - DeptWeeklyChart (new): per-person stacked bars with billing-category colours and red Forecast % line overlay. Click a name → DailyBreakdownModal with per-weekday stacked bars + red avg-% line. Click a bar segment → project-logs panel for that segment. - DeptBookingVsActual (new): grouped per-person bars (Active / Soft / Actual). - ResourceAvailability (new): sortable two-section table (FTE / Freelancer) showing each person's Active Booked, Soft Booked, Booked %, Logged h, and Status — the "booking + timesheet together" view. Forecast page: - 12 KPI tiles in two rows (Weekly Team Capacity → Active Next Week). - Last Week / This Week toggle + "Project Summary loaded" badge. - Weekly Pipeline chart: active-count bars + red exit-rate line + dashed forecast-avg baseline. Click a bar → drill into that week. - Right-hand "How this forecast is built" sidebar with prose explanations. Project Type Summary page: - "Project Type Benchmark" header + coverage callouts (months in timelog, warning about projects starting before coverage, recommended export range). - Month-preset pills. - Sortable Summary by Project Type table — 10 columns including avg assets/week and avg projects/month. - Per-type detail panel below: monthly assets trend chart + project list. - Bottom: Avg H/Asset and Avg Duration totals + Insights & Recommended Actions section with auto-generated outlier callouts. Backend additions: - /api/utilisation/department, /api/utilisation/daily-breakdown - /api/forecast extended with thisWeek + nextWeek capacity blocks - /api/project-types extended with monthly trends, project lists, longest-project tracking, coverage section, insights - services/department.py + tests/test_department.py - Booking model gains resourceRecordIds so daily breakdowns can match bookings by Airtable record ID, not just flattened name Tutorial selectors preserved + new ones added: dept-pills, resource-availability, forecast-sidebar, forecast-canvas. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| backend | ||
| deploy | ||
| frontend | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
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:<port>)
│
└─ 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 — seeapp/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.
- Go to https://airtable.com/create/tokens.
- Revoke the old token (
patHAB...) if it still exists. - Create a new Personal Access Token:
- Name:
utilisation-dept-backend - Scopes:
data.records:read - Access: limit to base
appoByydxIQANKtShonly, tablesResourceandBooking Resource.
- Name:
- Copy the token. You'll paste it into
.envbelow.
2. Generate secrets
# 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
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
# 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
docker compose up --build
# health: curl http://localhost:8200/api/health
Tests
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
ssh user@optical-dev.oliver.solutions
sudo git clone <repo-url> /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.conffrom the template. - Build the frontend →
/var/www/html/utilisation-dept/. - Build + start the Docker container.
- Health-poll
/api/health. - Print the Apache
Includeline for the shared vhost.
Wire into the shared vhost (one time only)
Add inside </VirtualHost> of
/etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf:
Include /opt/utilisation-dept/deploy/apache-utilisation-dept.conf
Then:
sudo apachectl configtest && sudo systemctl reload apache2
Smoke-check
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 https://optical-dev.oliver.solutions/utilisation-dept/ 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
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
- Generate a new PAT (same scopes as in setup).
- Edit
.env→ updateAIRTABLE_PAT. docker compose -p utilisation-dept restart backend- Revoke the old PAT in the Airtable portal.
Forgot admin password
- Generate a new bcrypt hash:
python3 -c "from passlib.hash import bcrypt; print(bcrypt.hash('new-password'))" - Edit
.env→ updateADMIN_PASSWORD_BCRYPT. docker compose -p utilisation-dept restart backend
Future work
- Azure AD / MSAL SSO:
app/auth/azure.pyis stubbed. FlipAUTH_MODE=azurein.env, fill inAZURE_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.