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>
Two production-readiness fixes called out in the scaling review
for ~40 concurrent users + daily upstream webhook traffic:
1. Prisma connection pool
DATABASE_URL now carries ?connection_limit=20&pool_timeout=10
both in docker-compose.yml (prod) and .env.example (local).
Default is cpus*2+1 (~5-9 inside a container) which can exhaust
at peak when mutations + TanStack Query polling coincide.
Postgres max_connections is 100 so 20 × a couple of app replicas
leaves headroom.
2. Webhook rate limiter
New src/lib/webhooks/rate-limit.ts — in-memory sliding-window
limiter keyed on "<scope>:<ip>". 100 req/min per IP per webhook
(omg / deliverables / briefs). Applied to all three POST
handlers; over the limit returns 429 with a Retry-After header.
Dev-mode bypass honours the matching *_WEBHOOK_ALLOW_INSECURE
env so stub testing isn't throttled.
Single-process only — swap the Map for Redis if we scale to
multiple Next.js instances. Single-instance dow-prod-tracker on
optical-dev is the target today, so in-memory is sufficient.
Also updated INTEGRATION.md with a rate-limiting section so the
upstream integrator knows what to expect + how to handle 429s.
Single-document spec for teams pushing Briefs, Projects, and
Deliverables. Each of the three gets its own section with:
- Webhook URL + signature header + HMAC signing example
(bash/python/node)
- REST equivalent (auth'd user path) for human-operator cases
- Annotated JSON body with every field, which are required, and
what they map to on our side
- Idempotency behaviour (externalId, omgJobNumber, project+name
natural keys)
- Error responses with the exact JSON shape they'll see
The doc is pitched at a dev at an upstream system, not a producer,
so it's spec-first: curl-ready examples, enum values spelled out,
header names called out in a table.