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>
105 lines
4.6 KiB
YAML
105 lines
4.6 KiB
YAML
name: loreal-prod-tracker
|
||
|
||
services:
|
||
# ─── PostgreSQL with pgvector ───────────────────────────
|
||
db:
|
||
image: pgvector/pgvector:pg17
|
||
restart: unless-stopped
|
||
environment:
|
||
POSTGRES_USER: postgres
|
||
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
||
POSTGRES_DB: loreal_prod_tracker
|
||
# Host port is overridable via DB_HOST_PORT env var — deploy.sh auto-picks
|
||
# a free one if 5492 is taken on the host. The container-internal port
|
||
# (5432) never changes — the app connects to db:5432 over the Docker
|
||
# network and doesn't care what host port (if any) is mapped.
|
||
ports:
|
||
- "${DB_HOST_PORT:-5492}:5432"
|
||
volumes:
|
||
- pgdata:/var/lib/postgresql/data
|
||
- ./docker/db-init.sql:/docker-entrypoint-initdb.d/01-pgvector.sql:ro
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||
interval: 5s
|
||
timeout: 5s
|
||
retries: 5
|
||
|
||
# ─── Next.js app ───────────────────────────────────────
|
||
app:
|
||
build:
|
||
context: .
|
||
dockerfile: Dockerfile
|
||
restart: unless-stopped
|
||
# Host port is overridable via APP_HOST_PORT env var — deploy.sh auto-picks
|
||
# a free one if 3002 is taken, and writes the chosen port into the Apache
|
||
# reverse-proxy config (apache/loreal-prod-tracker.conf) at the same time.
|
||
ports:
|
||
- "${APP_HOST_PORT:-3002}:3000"
|
||
environment:
|
||
# DATABASE_URL tuning knobs matter at ~40 concurrent users:
|
||
# connection_limit — how many pooled connections Prisma will
|
||
# open per app instance. Default is cpus*2+1 (~5-9 inside a
|
||
# container), which can run out at peak when mutations + query
|
||
# polling coincide. 20 gives plenty of headroom for this scale.
|
||
# pool_timeout — seconds to wait for a free connection before
|
||
# failing the request (default 10). 10s matches the default
|
||
# and stays explicit.
|
||
# Postgres side: default max_connections is 100, so 20 × a few
|
||
# app replicas is well below the ceiling.
|
||
DATABASE_URL: postgresql://postgres:${DB_PASSWORD:-postgres}@db:5432/loreal_prod_tracker?schema=public&connection_limit=20&pool_timeout=10
|
||
# Ollama — points to internal GPU server for embeddings + chat fallback
|
||
OLLAMA_HOST: ${OLLAMA_HOST:-http://10.24.42.219:11434}
|
||
OLLAMA_CHAT_HOST: ${OLLAMA_CHAT_HOST:-http://10.24.42.219:11434}
|
||
OLLAMA_CHAT_MODEL: ${OLLAMA_CHAT_MODEL:-gemma4:latest}
|
||
OLLAMA_EMBED_MODEL: ${OLLAMA_EMBED_MODEL:-nomic-embed-text}
|
||
NODE_ENV: production
|
||
AUTH_SECRET: ${AUTH_SECRET}
|
||
AUTH_TRUST_HOST: "true"
|
||
# Azure SPA registration — PKCE in browser, no client secret
|
||
AZURE_CLIENT_ID: ${AZURE_CLIENT_ID}
|
||
AZURE_TENANT_ID: ${AZURE_TENANT_ID}
|
||
AZURE_REDIRECT_URI: ${AZURE_REDIRECT_URI:-}
|
||
CRON_SECRET: ${CRON_SECRET:-change-me}
|
||
API_KEY: ${API_KEY:-}
|
||
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||
ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-}
|
||
DEV_BYPASS_AUTH: ${DEV_BYPASS_AUTH:-false}
|
||
DEV_USER_ID: ${DEV_USER_ID:-}
|
||
# OMG webhook (Shashank pending — stub until payload confirmed)
|
||
OMG_WEBHOOK_SECRET: ${OMG_WEBHOOK_SECRET:-}
|
||
OMG_WEBHOOK_ALLOW_INSECURE: ${OMG_WEBHOOK_ALLOW_INSECURE:-false}
|
||
# Auth: Entra SSO stays coded but gated. Flip to "true" post-MVP once redirect URI is live.
|
||
NEXT_PUBLIC_AUTH_ENTRA_ENABLED: ${NEXT_PUBLIC_AUTH_ENTRA_ENABLED:-false}
|
||
# Box integration (Phase 4). BOX_CONFIG_FILE points at the mounted
|
||
# JSON secret below. The other vars come from .env on the host.
|
||
BOX_CONFIG_FILE: ${BOX_CONFIG_FILE:-/run/secrets/box-config.json}
|
||
BOX_OUT_FOLDER_ID: ${BOX_OUT_FOLDER_ID:-}
|
||
BOX_WATCH_FOLDER_ID: ${BOX_WATCH_FOLDER_ID:-}
|
||
BOX_WEBHOOK_PRIMARY_KEY: ${BOX_WEBHOOK_PRIMARY_KEY:-}
|
||
BOX_WEBHOOK_SECONDARY_KEY: ${BOX_WEBHOOK_SECONDARY_KEY:-}
|
||
BOX_WEBHOOK_ALLOW_INSECURE: ${BOX_WEBHOOK_ALLOW_INSECURE:-false}
|
||
volumes:
|
||
- uploads_data:/data/uploads
|
||
secrets:
|
||
- box-config
|
||
depends_on:
|
||
db:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"]
|
||
interval: 15s
|
||
timeout: 5s
|
||
retries: 3
|
||
start_period: 30s
|
||
|
||
volumes:
|
||
pgdata:
|
||
uploads_data:
|
||
|
||
# Box JWT app config. Drop the JSON downloaded from the Box developer
|
||
# console at ./secrets/box-config.json on the host before `docker compose
|
||
# up`. Until that file exists, the app starts but Box features are
|
||
# disabled (isBoxConfigured() returns false; UI hides "Send to client").
|
||
secrets:
|
||
box-config:
|
||
file: ./secrets/box-config.json
|