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>
4.4 KiB
L'Oréal Studio Tracker — External API (v1)
Server-to-server API for creating projects and deliverables from external systems (e.g. OMG, brief intake forms, integration scripts).
Base URL: https://optical-dev.oliver.solutions/loreal-prod-tracker/api/v1
Auth
Send the shared API key on every request:
x-api-key: <API_KEY>
The key matches process.env.API_KEY on the server. Requests with a
missing or wrong key get 401 Unauthorized.
Optionally specify which organization the request applies to:
x-org-id: <organization-id>
If omitted, requests apply to the first organization in the database (useful in single-org deployments).
Idempotency
POST endpoints honour Idempotency-Key. A retried request with the same
key + same payload returns the cached response without re-executing:
idempotency-key: 7f3c8e2a-...-deterministic-uuid
The same key with a different payload returns 409 Conflict. Records
expire after 24 hours.
Endpoints
GET /api/v1/projects
List all projects visible to the caller. Returns the same shape as the
in-app /api/projects.
POST /api/v1/projects
Create a project. If pipelineTemplateId is omitted, the org's default
pipeline template is auto-applied. Body matches createProjectSchema
(see src/lib/validators/project.ts).
Required fields:
projectCode(string, unique per org)name(string)
Recommended fields:
omgJobNumber(string) — enables Box-folder matching downstreamclientTeamId(string) — drives team visibilityrequestorUserId(string) — project ownerpriority(LOW|MEDIUM|HIGH|URGENT)status(PIPELINE|ACTIVE| …)startDate,dueDate(ISO 8601)
Returns 201 + the created project.
curl -X POST \
-H "x-api-key: $API_KEY" \
-H "idempotency-key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{
"projectCode": "API-001",
"name": "Test integration project",
"omgJobNumber": "12345",
"priority": "MEDIUM"
}' \
https://optical-dev.oliver.solutions/loreal-prod-tracker/api/v1/projects
GET /api/v1/projects/{projectId}
Read-back. Same shape as in-app /api/projects/{id} — includes
deliverables, stages, attachments, requestor user, client team.
POST /api/v1/projects/{projectId}/deliverables
Create a deliverable on the given project. Pipeline stages auto-applied
from the project's template (Deliverable.pipelineTemplate cascades to
DeliverableStage rows). Body matches createDeliverableSchema.
Required fields:
name(string)
Common fields:
priority,dueDate,notes,externalId(idempotency for upstream webhook callers — distinct from theIdempotency-Keyheader)
Returns 201 + the created deliverable with its stage chain populated.
curl -X POST \
-H "x-api-key: $API_KEY" \
-H "idempotency-key: $(uuidgen)" \
-H "content-type: application/json" \
-d '{
"name": "Hero banner",
"priority": "HIGH",
"dueDate": "2026-06-30T12:00:00Z"
}' \
https://optical-dev.oliver.solutions/loreal-prod-tracker/api/v1/projects/$PROJECT_ID/deliverables
Error shapes
{ "error": "Project code \"X\" already exists" }
All errors return JSON with an error string. Status codes:
400— validation error (zod issues joined into one message)401— missing/wrong API key404— project not visible (RBAC) or not found409— Idempotency-Key reused with a different payload500— server error (check server logs)
What's NOT here
- Update/delete endpoints — out of scope for v1. External callers create; in-app users update.
- Deliverable read-back —
GET /api/v1/projects/{id}includes deliverables in its response. A dedicated deliverable read endpoint can be added if external callers need it. - Bulk endpoints — call POST per project/deliverable.
Internal notes
- Auth path:
src/middleware.tsshort-circuits API-key requests before the session check.src/lib/api-utils.tsgetAuthSession()resolves the key into a synthetic ADMIN session for the chosen org. - Idempotency:
src/lib/api/idempotency.ts+IdempotencyRecordprisma model. 24-hour TTL — sweep via cron (seescripts/). - Default pipeline fallback:
src/lib/services/project-service.tscreateProject()— looks up the org'sPipelineTemplate.isDefaultif nopipelineTemplateIdis supplied.