Four phases shipped together. Each is a logical deploy unit on its own;
keeping the diff atomic so the rename runbook + migrations stay aligned.
Phase 1 — restore HP's formal review workflow
- Prisma: FeedbackItem, ReviewSession, ReviewSessionItem + enums
- New ApprovalType (NONE | SIMPLE | FORMAL) on PipelineStageDefinition
and PipelineStageTemplate. Stage row UI branches per type.
- feedback-service + review-session-service ported from HP (no ColorProbe)
- annotation-service auto-creates a FeedbackItem; revision-service
carries forward unresolved action items into the new revision.
- API: /api/reviews/*, /api/stages/[id]/feedback, /api/feedback/[id]
- Hooks: use-feedback, use-review-sessions
- UI: feedback-checklist, feedback-item-card, feedback-progress-bar,
create-session-dialog, session-builder, session-presenter,
session-summary, plus a new stage-review-panel
- Pages: /reviews list + detail, deliverable annotation review page
- Pipeline editor gets the approvalType select; sidebar gets Reviews
Phase 2 — full Dow Jones → L'Oréal rebrand + slug rename
- URL slug /dow-prod-tracker → /loreal-prod-tracker (next.config,
base path, redirects)
- docker-compose name + DB → loreal_prod_tracker; server path
/opt/loreal-prod-tracker; apache template renamed
- All visible strings → L'Oréal; sidebar bg #002B5C → black
- docs/RENAME_RUNBOOK.md describes the one-shot server migration
- Internal modules dow-excel-service/dow-import + OMG webhook domain
dowjones.com deliberately preserved (orthogonal to the rebrand)
Phase 3 — external /api/v1 for projects + deliverables
- API-key auth already in middleware; finished idempotency support
via new IdempotencyRecord model + src/lib/api/idempotency.ts
- Default-pipeline fallback in createProject when no template id given
- POST/GET /api/v1/projects + POST /api/v1/projects/[id]/deliverables
- docs/EXTERNAL_API.md with curl examples
Phase 4 — Box bidirectional integration
- JWT app-auth via jose (no extra deps). Config mounted as a docker
compose secret; deploy.sh stubs an empty {} so compose can start
before the operator drops the real JSON.
- Outbound: pushDeliverableToBox auto-fires on !APPROVED → APPROVED
in deliverable-status-service; "Send to client (Box)" manual button
on the approval stage row. Folder naming
{omgJobNumber}_{slug}_v{round}. 3-attempt exp backoff. BoxPushLog
audit.
- Inbound: /api/webhooks/box receives Box's signed events, matches by
OMG # + slug, creates a new Revision, routes to assignee or notifies
project owner. BoxInboundLog audit + two new NotificationType
values (BOX_UNMATCHED_FILE, NEW_FILE_AWAITING_REVIEWER).
- Naming-convention logic isolated in external-delivery-service so an
OMG-API transport can swap in later without touching matchers.
- Admin /settings/box page surfaces config status + recent activity.
Three Prisma migrations to apply on next deploy:
20260512000000_restore_review_workflow
20260512100000_idempotency_records
20260512200000_box_integration
URL rename is a one-shot — see docs/RENAME_RUNBOOK.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
You were right — everything's containerized, the host ports are just
reverse-proxy targets (+ an optional psql peephole for the db). Hardcoding
them is why the local smoke test face-planted on 5492 (amazon-transcreation
was squatting it) and would have done the same any time anything else
bound :3002 or :5492 on the shared server.
docker-compose.yml:
- ports now reference `${APP_HOST_PORT:-3002}` and `${DB_HOST_PORT:-5492}`.
Defaults match the prior-committed values; override via env vars.
Container-internal ports (3000, 5432) never change.
apache/dow-prod-tracker.conf → .conf.tmpl:
- Moved to a committed template with `${APP_HOST_PORT}` placeholders in
both the WebSocket rewrite and the ProxyPass/ProxyPassReverse lines.
- deploy.sh renders the real .conf from the template on every run with
the chosen port substituted in. Rendered .conf is gitignored so it
can vary per server without drift.
deploy.sh:
- New is_port_free() and find_free_port() using bash's /dev/tcp — no
external tool dependency, works identically on Ubuntu and macOS.
- After `docker compose down` (which frees any of OUR ports), probe for
APP_HOST_PORT starting from 3002 and DB_HOST_PORT from 5492. Pick the
first free port (scan up to 50). Warn if the preferred port was busy.
Honors explicit override: `APP_HOST_PORT=3005 ./deploy.sh` works.
- Exports the chosen ports before `docker compose up` so compose
substitutes them into the `ports:` mappings.
- Renders apache/dow-prod-tracker.conf from the .tmpl with the same
APP_HOST_PORT, every deploy. If the Apache Include line is already in
the vhost, we reload Apache anyway (picks up the re-rendered snippet
in case the port changed).
- Health check URL uses APP_HOST_PORT.
- "Deploy complete" banner now prints the chosen ports.
.gitignore:
- Added docker-compose.override.yml (per-machine local overrides) and
apache/dow-prod-tracker.conf (rendered by deploy.sh, varies per server).
DEPLOY.md updated with the auto-detection behaviour and override recipe.
Sanity-checked locally:
- is_port_free correctly identifies 5492 busy (amazon-transcreation),
5493 busy (our smoke-test db), 3002 busy (Docker Desktop grabs 3000-3002
on this Mac), and picks 5494/3003 respectively.
- `APP_HOST_PORT=3999 DB_HOST_PORT=5999 docker compose config` produces
published ports 3999 and 5999.
- `bash -n deploy.sh` clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Deploy fixes (critical — Phase 0 string-rebrand didn't touch numeric ports):
- deploy.sh APP_PORT 3001 → 3002 (health check was hitting HP's app!)
- apache/dow-prod-tracker.conf — all proxy/websocket rules 3001 → 3002
(traffic to /dow-prod-tracker would have been served by HP's container)
- deploy.sh: added COMPOSE_PROJECT=dow-prod-tracker and `-p $COMPOSE_PROJECT`
on every `docker compose` invocation (down, up, exec, logs, ps). This is
the CLAUDE.md belt-and-braces rule — without it, a future move of the
deploy dir to `deploy/` would collapse the compose project name to
`deploy` and collide with any other app in a sibling `deploy/` dir on
the shared server. The `name:` field in compose covers us today, -p
covers us tomorrow.
- apache conf header comment rewritten to explain the port convention and
where to keep it in sync.
Admin add-user flow (answers the open question):
- createInvitation now creates/upserts the placeholder User row
(email + role + organizationId + isExternal + mustChangePassword=true)
in addition to the Invitation bookkeeping row. It stores a 24-byte
password-reset token on BOTH the User (passwordResetToken) and the
Invitation (token) — same token, so the existing /reset-password/[token]
page accepts the invite URL without a separate accept endpoint.
- Role enum now includes CLIENT_VIEWER. isExternal auto-derives from role
but can be overridden. When admin invites a CLIENT_VIEWER, the placeholder
user lands correctly pre-flagged for external handling.
- POST /api/org/invitations now returns {acceptUrl} — the full
/reset-password/<token> link admin can hand over out-of-band while SMTP
is unwired.
- revokeInvitation also clears the reset token on the placeholder user so
a leaked URL can't be used to claim the account after revocation.
- Deleted /api/invitations/accept (SSO-era — the accept IS the password
reset now) and removed acceptInvitationSchema from the validator.
Team settings UI (src/app/(app)/settings/team/page.tsx):
- Role dropdown now has "Client (read-only)" alongside Admin/Producer/Artist.
- After a successful invite, a banner shows the accept URL with a Copy
button so admin can paste it into Teams/email. Dismissible.
- Current-members list renders CLIENT_VIEWER with an amber badge.
Plumbing verified: tsc --noEmit ✓ zero errors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- basePath /dow-prod-tracker, DB name dow_prod_tracker
- docker-compose: name: dow-prod-tracker (volume isolation on shared server), ports 3002/5492
- OMG webhook env vars (secret + insecure toggle)
- NEXT_PUBLIC_AUTH_ENTRA_ENABLED feature flag (MVP uses local auth)
- Dow logo at public/navbar-logo.png
- apache/hp-prod-tracker.conf → apache/dow-prod-tracker.conf
- Text rebrand across README, SETUP, CLAUDE.md, docs, UI labels
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Token exchange now happens entirely in the browser via @azure/msal-browser
(PKCE, no client_secret — correct for Azure SPA registrations)
- Browser stays on /hp-prod-tracker/login throughout; the /api/auth/callback
URL never appears in the address bar
- New /api/auth/sso route validates the id_token (jose + Azure JWKS),
creates User/Account/Session in Prisma, and sets the authjs session cookie
- Auth.js retained only for session reading (auth()) and signOut()
- Fix dev bypass safety gate: use NODE_ENV !== production instead of
absence of AUTH_MICROSOFT_ENTRA_ID_SECRET
- Rename env vars: AUTH_MICROSOFT_ENTRA_ID_ID → AZURE_CLIENT_ID,
AUTH_MICROSOFT_ENTRA_ID_TENANT_ID → AZURE_TENANT_ID, remove AUTH_URL
- Remove /api/auth Apache proxy rule (no longer needed)
- Delete OAuthRelay.tsx, add MsalLogin.tsx
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
next-auth v5 beta.30 cannot reliably pass the /hp-prod-tracker prefix
through OAuth redirect_uri — redirectProxyUrl is silently ignored.
Instead: AUTH_URL=https://…/api/auth (matches basePath exactly), Auth.js
sends consistent redirect_uri in both authorization and token exchange,
Apache proxies /api/auth → :3001 before the OliVAS /api/ rule.
Azure must have https://optical-dev.oliver.solutions/api/auth/callback/microsoft-entra-id registered.
Server .env: AUTH_URL=https://optical-dev.oliver.solutions/api/auth
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Apache config on this server is managed manually in optical-dev.oliver.solutions.conf
(same pattern as cc-dashboard). Deploy script no longer touches Apache.
Config moved to apache/hp-prod-tracker.conf matching amazon-transcreation pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>