Commit graph

218 commits

Author SHA1 Message Date
DJP
bb9c7aa02d Box inbound: defensive loop guard on parent folder
Reject inbound events whose Box file isn't a direct child of the
configured BOX_WATCH_FOLDER_ID, and explicitly skip any file whose
parent IS BOX_OUT_FOLDER_ID. Box webhooks are folder-scoped already
so a normal subscription won't fire on our own outbound writes —
this is defence in depth in case a future webhook is mis-subscribed
or the two folder ids ever drift into the same value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:45:19 -04:00
DJP
545b0eef05 Stage review panel: "Add first version" + always-visible Start review
Two small UX fixes for manual-start clarity on FORMAL stages:

1. "New revision" button label is "Add first version" when the stage
   has no revisions yet (was the cryptic "+ V0.1"). After the first,
   subsequent labels keep the "+ V0.2", "+ V0.3" pattern. The first
   button is also rendered as primary (default variant) to make the
   start-here action obvious.

2. "Open review" is now visible on FORMAL stages whenever the
   approvalType is set, not only when a revision exists. The label
   flips to "Start review" when there are no revisions yet, so
   producers can navigate to the review surface immediately and
   upload from there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:32:27 -04:00
DJP
630570caeb Review page: full HP markup surface restored on the single-asset model
Replaces the transitional stub with the full HP-style annotation surface
adapted to one-asset-per-revision. Image + video annotation, full
toolset (rect / ellipse / arrow / freehand / text / pin / screenshot),
cross-revision comparison (any pair from the chain, defaults latest vs.
prior), all comparison modes (side-by-side / wipe / overlay / toggle),
video player with timeline scrubbing + frame-aligned annotation markers,
sidebar (revision timeline + feedback checklist), keyboard shortcuts,
read-only path for CLIENT_VIEWER.

Adaptation from HP:
  - HP read attachments.{current,reference,video,referenceVideo,screenshot}.
    Collapsed to revision.asset (single image OR video, MIME-detected).
  - HP compared current-vs-reference within a single revision. Now
    enumerates one RevisionOption per revision and compares any pair
    from the chain.
  - ColorProbe / CMF eyedropper dropped (out of scope for L'Oréal).
  - ImageGallery + image/video toggle removed (one asset per revision
    makes them redundant; the sidebar timeline lists every revision).
  - AssetUploadZone (the unified zone) replaces the four HP upload zones.
  - Send-to-client wired into the top bar via useSendToClient.
  - Query params: ?stage= and ?revision= both preselect.
  - Polls every 3s while any video is still "processing" so the HLS
    transcode result auto-renders when ready.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:06:34 -04:00
DJP
128b5d1d93 Review page: wire AnnotationLayer overlay back onto image revisions
Image revisions now render with the full annotation toolset on top of
the asset (rectangle / ellipse / arrow / freehand / text / pin /
screenshot). Tracks natural image dimensions on load and resizes the
overlay alongside the displayed copy via ResizeObserver, so SVG
coordinates stay aligned even when the image is scaled down to fit
the viewport.

Read-only for CLIENT_VIEWER. Video annotation + cross-revision
comparison still pending — image is the common case so it ships first.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:49:34 -04:00
DJP
98ebe4796d Review page: add upload zone when latest revision has no asset
Surface the AssetUploadZone directly on the deliverable review page
when the latest revision is asset-less, plus a "Replace asset" toggle
when one is already uploaded. CLIENT_VIEWER stays read-only.

Closes the gap where producers were stuck on "No asset uploaded yet
for V0.2" with no way to upload from this page (they had to go back
to the deliverable detail panel).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:35:34 -04:00
DJP
45dfdcad23 createDeliverable: auto-mirror dynamic stages into legacy seed table
DeliverableStage.templateId is a NOT-NULL FK to pipeline_stage_templates
(HP-era seed). On a fresh DB with no seed run, that table is empty —
so even with a dynamic pipeline attached, the FK has nothing to point
at and stage creation silently fails (deliverable lands with 0 stages).

When createDeliverable detects this state, mirror each dynamic stage
definition into pipeline_stage_templates (idempotent upsert on slug)
so the FK is satisfied. No schema change; no operator action required.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:22:19 -04:00
DJP
4aa711c7ae createDeliverable: fall back to org default pipeline when project has none
If a deliverable is created on a project that has no `pipelineTemplateId`,
look up the org's default `PipelineTemplate` and use its stages — mirrors
the existing fallback in `createProject`. Also persists the chosen
template back onto the project so future deliverables don't re-resolve
and the deliverable detail UI can show "Pipeline: …".

Closes the gap where a deliverable created before a pipeline was attached
to its project landed with zero stages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:12:03 -04:00
DJP
bdb133d49a Phase 5: unified versioning + single-asset-per-revision + holding pen
Reshapes the Revision model around how producers + clients actually
work: one upload per round (image OR video, no references), per-stage
version chain V0.1 → V0.2 → V1 → V1.1 → V1.2 → V2 …, holding pen for
inbound files received while a previous version is in client approval,
and per-pipeline filename matching for inbound ingest.

Schema:
  - Revision: drop roundNumber + multi-key attachments. Add major/minor/
    sentToClient/sentAt/asset (single image-or-video object) + unique
    (deliverableStageId, major, minor).
  - PipelineTemplate: add inboundFilenameRegex (per-pipeline matcher).
  - NEW HoldingPenFile model + HoldingPenSource enum (MANUAL/API/BOX).
  - Empty prod DB → clean ALTER TABLE migration, no backfill.

Send-to-client semantics:
  - The latest internal revision IS the V{n} — promote in place
    (major+=1, minor=0, sentToClient=true). Chain reads V0.1, V0.2, V1,
    V1.1, V1.2, V2, ...
  - sendToClient is the ONLY Box-push trigger. Auto-on-APPROVED removed
    from deliverable-status-service. APPROVED is now an internal "done
    iterating" state, separate from "shipped to client".

Three input channels, one matcher:
  - NEW src/lib/services/inbound-ingest-service.ts — consolidates Box
    webhook, /api/v1/upload, and manual upload-by-filename. One regex
    resolver, one project/deliverable matcher, one routing + notification
    fan-out.
  - box-inbound-service is now a thin wrapper that fetches Box metadata
    and delegates.
  - external-delivery-service.parseInboundFileName takes an optional
    regex override. Default: ^(\d+)_([a-z0-9-]+)(?:_v(\d+))?(?:\.[a-z0-9]+)?$
    captures (1) OMG #, (2) slug, (3) optional version.
  - buildDeliveryNaming now uses {omg}_{slug}_V{major} for Box folders.

Holding pen:
  - When a deliverable is IN_REVIEW (a V{n} is awaiting client decision)
    and a new file arrives via any channel, it lands in HoldingPenFile —
    NOT in the active chain. Producer manually promotes (creates the
    next minor on the chosen stage) or discards.
  - Held files render in a new HoldingPenPanel on the deliverable detail
    page when present. Source pill (MANUAL/API/BOX), parsed identifiers,
    target-stage picker, Promote + Discard buttons.

Per-pipeline regex UI:
  - NEW InboundMatchingRules section in the pipeline editor. Live regex
    compile, sample-filename match test with echoed captures, save with
    server-side regex-compile validation.

Upload simplification:
  - storeRevisionAsset replaces the old processAndStoreImage +
    processAndStoreVideo + multi-key attachment merge. MIME-detects kind,
    preserves PNG alpha flatten + TIFF→PNG + thumbnail for images, and
    keeps the async HLS transcode pipeline for videos.
  - The single revision upload route drops the `type=` parameter.
  - Three legacy components deleted: image-gallery, image-upload-zone,
    video-upload-zone. NEW asset-upload-zone (unified drop zone).

New API:
  - POST /api/stages/:stageId/revisions/:revisionId/send-to-client
  - GET  /api/deliverables/:id/holding-pen
  - POST /api/deliverables/:id/holding-pen/:fileId (promote)
  - DELETE /api/deliverables/:id/holding-pen/:fileId (discard)
  - POST /api/v1/upload (multipart; same matcher as Box webhook)

UI label rollup:
  - src/lib/format-revision-label.ts is the single source of truth.
    Sent revisions render `V{major}`; internal `V{major}.{minor}`.
  - Revision node/timeline, session presenter/builder/summary, revision
    list, stage review panel — all read the helper.
  - Comparison toolbar simplified to cross-revision picker (no more
    reference-vs-current within a single revision).

The deliverable annotation review page is a temporary stub (links back
to the deliverable detail page where the in-row controls live). The
full annotation overlay + comparison surface will be rebuilt against
the single-asset model in a follow-up.

Run on next deploy:
  docker compose -p loreal-prod-tracker exec app npx prisma migrate deploy

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 19:37:06 -04:00
DJP
e8eeb79d9c Sidebar: text wordmark in place of the old navbar PNG
The legacy navbar-logo.png still shipped the previous client's graphic.
Until a real L'Oréal logo asset is supplied, render the wordmark as
text — white on the black sidebar background, both desktop and mobile.

Drop a navbar-logo.png into public/ later and swap back to <img> if you
want the graphic back.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:36:02 -04:00
DJP
4362f588dd Add scripts/create-admin.ts — one-shot reset + admin user create
Wipes all other users and upserts a single admin with the given email +
password. Use after the rename when you want a clean slate.

  docker compose -p loreal-prod-tracker exec app \
    npx tsx scripts/create-admin.ts admin@loreal.com 'YourPasswordHere'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 18:25:57 -04:00
DJP
1b73d6b8db L'Oréal rebuild: restore review workflow, full rename, /api/v1, Box integration
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>
2026-05-12 17:51:53 -04:00
DJP
ae6ebc6da2 Deliverables board: drag highlight, pipeline filter, "in pipelines" footer
Three connected improvements on the Deliverables board:

1. Drag highlight. When you start dragging a card, every column
   previews whether a drop would be allowed. Same rules as the
   server validator:
     - forward-by-order (target.order > current.order)   → green
     - backward + declared rework edge exists            → green
     - same column                                        → no-op, no tint
     - backward without a rework edge                     → red
     - cross-pipeline / "__none__" bucket                 → red
   Columns the drop can't target also fade to 50% opacity to pull
   focus toward the valid ones. Status mode is always green since
   Deliverable.status is a direct write.

   Drag validity uses rework edges pulled live from the selected
   pipeline's detail (usePipelineTemplate), so configurator changes
   in /settings/pipelines show up immediately on the board.

2. Pipeline filter. New "All Pipelines" select in the filter row,
   shown only when >1 pipeline template exists (noise otherwise).
   When set, filters deliverables by project.pipelineTemplateId AND
   drives the board columns + rework edges from that pipeline.

3. "In pipelines" footer. At the base of each stage column, shows
   which pipeline templates include that stage slug — but only when
   the stage is shared across ≥2 pipelines. Single-pipeline labels
   would be redundant.

Plumbing:
- getAllDeliverables service + AllDeliverableRow now include
  project.pipelineTemplateId so the page can filter without a
  second round-trip.
- DeliverableBoard accepts two new optional props: pipelineStages
  extended with reworkToSlugs (per-stage rework targets) and
  allPipelines (list of { id, name, stageSlugs } for the footer).
- Deliverables page computes boardPipelineStages from the active
  pipeline's detail (falls back to the default when filter is
  "all") and allPipelinesSummary from the list response.
2026-04-22 12:53:40 -04:00
DJP
bcd1b245bd Calendar → Due Date Calendar (simplified, stage spans gone)
Old calendar plotted every in-flight stage of every deliverable as
a horizontal span. With Dow's 11-stage pipeline that's 4+ bars per
deliverable per week — unreadable after just a handful of jobs.

Rewrote as a month grid of due-date pills:
- One pill per deliverable on its due date, coloured by priority.
- Filters: project + priority.
- Prev / Today / Next month nav.
- Click a pill → deliverable detail.
- Cells show up to 4 pills + "+N more" overflow indicator.

Plumbing:
- calendar-view.tsx rewritten top-to-bottom (fresh ~230 LOC).
  Uses the existing useAllDeliverables hook — no new API surface.
- Deleted ~1300 LOC of orphaned components: calendar-grid,
  calendar-event-pill, calendar-day-detail, calendar-heatmap,
  calendar-filters. Plus the /api/calendar route and
  use-calendar hook (nothing else referenced them).
- Page title + breadcrumb label → "Due Date Calendar".
- Sidebar nav label → "Due Dates" to save space.

Net −1100 LOC. The calendar now earns its place ("what's due this
week?") without fighting the Timeline view for the Gantt job.
2026-04-21 20:25:43 -04:00
DJP
28b30c60b4 Pipeline clone copies reworks + editor gets a Duplicate button
duplicatePipelineTemplate was written before the PipelineStageRework
table existed, so cloning a pipeline silently lost every declared
pass/fail pushback rule. Board-drag rework on the clone would then
reject every backward move because no rework edges existed. Now
copies reworkFrom edges alongside dependsOn, reusing the old→new
stage-id map so remapping is free.

Also added a "Duplicate" button to the pipeline editor header
(next to "Set Default") so admins editing a pipeline can fork and
edit the copy without going back to the /settings/pipelines list.
On success we push into the new clone — the admin's next action
is almost always "tweak the copy", no need to make them navigate.

Existing "Duplicate" button on the list page (per-card hover)
stays — this just adds a second entry point.
2026-04-21 20:15:52 -04:00
DJP
d5d8c7560a Self-assignments fire notifications too
Previously notifyAssignment bailed early when userId ===
assignedByUserId on the logic "you just did it, you know".
But producers use the /notifications page as their source-of-
truth for "everything I'm on", and a self-pick silently not
landing there broke that mental model.

Always notify now. Copy branches to match the case:
  other → "New assignment" / "You were assigned to …"
  self  → "You picked up an assignment" / "You assigned
          yourself to …"

Also keeps the notification bell unread counter accurate — was
always 0 after someone self-assigned a dozen stages.
2026-04-21 20:11:26 -04:00
DJP
47a65d6498 Keep Deliverable.status in sync with stage state
Root cause of the mismatch: Deliverable.status is a denormalised
column that was only written at create-time (default NOT_STARTED)
and never refreshed when stages moved. The Projects board read it
live and showed "Not Started" while the pipeline ring + dominant-
stage view correctly showed "at Client Feedback (6/11 stages
complete)".

Fix in two parts:

1. New deliverable-status-service with:
   - computeDeliverableStatus(stageStatuses[]) — pure function with
     the summary rule:
        all stages terminal              → APPROVED
        any IN_REVIEW                    → IN_REVIEW
        any IN_PROGRESS/CHANGES_REQUESTED → IN_PROGRESS
        else                             → NOT_STARTED
     ON_HOLD is producer-managed and never overwritten.
   - recomputeDeliverableStatus(deliverableId, txClient?) —
     executes the rule + writes if different. Accepts an optional
     Prisma tx client so callers can run inside their own
     transaction.

2. Wired into every stage-write path:
   - stage-service.updateStageStatus (single-stage transitions)
   - stage-service bulk transaction (bulkUpdateStages) — dedups
     touched deliverable IDs so we don't recompute twice.
   - stage-transition-service forward + rework (board drag) —
     inline inside the same $transaction so the board bucket is
     correct on the next refetch.

3. Backfill script scripts/recompute-deliverable-statuses.ts —
   one-off sweep to fix existing stale rows:
     npx tsx scripts/recompute-deliverable-statuses.ts
   Run once after deploy.
2026-04-21 19:55:39 -04:00
DJP
926225a05b Breadcrumbs show names + OMG number instead of raw cuids
Path like /projects/cmo8zgk46000g01rt473bhbdy/deliverables/cmo8zj688...
used to render the opaque cuid segments verbatim, giving a header
like "Projects / cmo8zgk46000g01rt473bhbdy / Deliverables / ...".
Breadcrumb now detects the ID segments by position (any segment
right after "projects" / "deliverables" / "briefs") and swaps in
the human name.

- Projects → "#<OMG#> — <name>" when omgJobNumber is set, bare name
  otherwise. Most Dow projects come in with an OMG number so the
  prefixed form is the common case.
- Deliverables → name only (no OMG equivalent on deliverables).
- Briefs → title.

Queries are conditional on the URL shape and share cache keys that
won't collide with the page-level useProject / useDeliverable /
useBriefs hooks. Page visits usually hit the same endpoints first,
so the crumb resolves from cache with no extra round-trip. Truncate
overlong titles at 240/320px with a title attr for the full string
on hover.
2026-04-21 19:44:45 -04:00
DJP
5847580bee Project Owner is now a User, not freeform text
Edit Project dialog's Owner/Project Manager field is now a dropdown
of real users on the system instead of a text box — so the value is
a FK to a real User row rather than a hand-typed string that drifts
from the roster.

Schema:
- Project.requestorUserId (nullable FK → User) + relation
  "ProjectOwner" on both sides. Migration 20260425000000_project_owner_fk.
- Existing freeform `requestor` column stays put. It's now the
  FALLBACK for ingest-sourced rows where the upstream Owner name
  doesn't match a user on the system yet; the UI prefers the linked
  user's display name when one exists.

Plumbing:
- createProjectSchema accepts requestorUserId.
- project-service listProjects + getProject both include
  requestorUser { id, name, email } so the UI doesn't need a second
  round-trip.

UI:
- ProjectFormDialog swaps the Input for a Select populated by
  useUsers(). "— Unassigned —" at the top clears the link. When a
  project has a legacy freeform requestor and no linked user, the
  dialog shows it as a hint ("Currently unlinked (from intake):
  <name> — pick a user from the list to replace") so admins can
  see what came in from the XLSX/webhook and bind it.
- Project detail view + list view both prefer
  requestorUser.name ?? requestorUser.email ?? requestor for the
  "Owner" column. Sort key on the list uses the same chain.
2026-04-21 19:43:14 -04:00
DJP
1445fe2c1d Strip HP fields from project dialog + view + make Deliverable editable
User flagged the Edit Project dialog showed fields that weren't on
the detail view (and vice versa), and that several fields were
HP-era leftovers that Dow doesn't use at all (Form Factor, Quarter,
Agency, NPI/Refresh, costs). Same complaint: couldn't edit
deliverable info even as admin.

Project dialog:
- Dropped the "References" tab entirely. Workfront ID + BMT ID are
  HP-era tracking fields Dow doesn't use; keeping them made the
  form disagree with the detail view.
- Moved "Owner / Project Manager" from References → Details so it's
  alongside the other first-class fields (OMG#, team, category,
  status, priority, owner).
- Dialog is now two tabs: Details + Dates. Fields stay in lockstep
  with the view.

Project detail view:
- Trimmed the metadata grid to match the dialog 1:1: OMG Job #,
  Project Code, Client Team, Status, Priority, Owner, Category,
  Brief Accepted, Due Date. Dropped Quarter, Form Factor, Code
  Name, NPI / Refresh, Agency, Workfront ID, OMG Code, BMT ID,
  Estimated Cost, Actual Cost — those stay in the schema for
  back-compat but no longer clutter the UI.
- Edit button's defaultValues trimmed to match.

Deliverable detail:
- New Edit button on the "Deliverable Info" panel (admin-only,
  same canEditAttachments gate as the attachments panel).
- Reuses the existing DeliverableFormDialog — now smart about
  edit mode (derives from prefilled name; submit button says
  "Save"/"Saving" instead of "Create"/"Creating", More Fields
  section auto-opens so producers can see everything on edit).
- Dropped "WF Input Date" from both the form and the view — HP
  Workfront intake date, not part of the Dow workflow.
- Edit dialog pre-fills from the loaded deliverable, submits via
  the existing useUpdateDeliverable hook, toasts on success/fail.
2026-04-21 17:28:02 -04:00
DJP
13e069d72c Hardening: Prisma pool bump + webhook rate limiting
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.
2026-04-21 17:01:19 -04:00
DJP
307619ffe6 Nightly pg_dump backups + admin "Export Full XLSX" button
Two complementary safety nets for the business-critical DB:

1. Host-side nightly backup
   - scripts/backup-db.sh drives pg_dump through docker compose exec,
     gzips to /srv/backups/dow-prod-tracker/, auto-prunes >30 days.
     Env-overridable (BACKUP_DIR / RETAIN_DAYS / COMPOSE_DIR / PGUSER
     / PGDATABASE) for anyone running a different layout.
   - Runs from host cron at midnight; crontab snippet + restore
     procedure + optional off-site (S3 / rsync) pattern documented
     in DEPLOY.md.

2. On-demand admin XLSX export
   - New GET /api/projects/export?format=xlsx — ADMIN-only; builds a
     two-sheet workbook via new buildFullExportWorkbook():
       - "Job Tracker": one row per project, header strings chosen to
         round-trip through the Dow bulk-import endpoint so a dump can
         be re-ingested in a worst case. Owner / Risk / OMG Number /
         Team / etc., mirroring the importer's fuzzy HEADER_MATCHERS.
       - "Deliverables": one row per deliverable with project OMG #,
         status, priority, dates, CMF/SKU, current stage, assignees,
         notes — enough to reconstitute pipeline state.
     Respects visibility scoping (ADMIN sees everything).
   - Dashboard shows an "Export Full XLSX" button in the header for
     admins; streams the workbook with a date-stamped filename using
     the standard blob-download pattern from ExportButton.

Both are additive — no schema, no migration, no deploy breakage.
2026-04-21 16:53:49 -04:00
DJP
9b0156afc8 Project detail: always show all fields + admin-editable + owner
Three fixes the user flagged on the /projects/:id page:

- Metadata panel rendered only populated fields; empty ones vanished
  entirely. A freshly-created project showed "Business Unit: Display"
  and nothing else, making admins think half the fields didn't exist.
  Now all fields always render with an em-dash ("—") for empty —
  obvious what's missing, and fmt helpers keep dates/money clean.
- Default the collapsible "Project Details" to OPEN. Was collapsed,
  which hid the whole form behind a click.
- New Edit button inside the details panel header (admins only).
  Reuses the existing ProjectFormDialog in edit mode with
  defaultValues populated from the loaded project; submit goes via
  useUpdateProject (already existed). Toast on success/error.
- "Owner" is now a labeled field in the panel — maps to the
  existing Project.requestor column (same field the XLSX intake
  column is named "Owner"; the form dialog already labeled it that
  way but the read view called it "Requestor").

Also added Description block above the metadata grid so narrative
context isn't hidden behind the Edit dialog.
2026-04-21 16:46:41 -04:00
DJP
02593ece83 Stage-tagged attachments — per-stage paperclip indicator + general panel
Splits the attachments experience in two:

Top panel (AttachmentsPanel):
- Renamed "Attachments & Links" → "General Assets" with a subtitle
  calling out the split ("Applies to the whole deliverable —
  stage-specific assets live on each pipeline stage").
- List filtered to only show attachments with
  stageDefinitionId == null. Uploads from here default to no-stage.
- Empty-state copy updated to describe the new purpose.

Per-stage indicator (new StageAttachmentIndicator):
- Small paperclip icon + count rendered on each stage row, right
  alongside the status badge.
- Zero + canEdit: paperclip + small "+" so the affordance to add is
  still visible. Zero + read-only: component renders nothing so
  CLIENT_VIEWERs don't see empty affordances they can't act on.
- Click opens a Popover listing attachments tagged to that stage,
  with inline upload + link-add buttons at the bottom. Uploads auto-
  tag to the stage via useUploadFileAttachment({ stageDefinitionId })
  — the mutation already accepted this field, no API change needed.
- Delete allowed on rows from the popover, same canEdit gate.
- Figma embeds preview inline in the popover just like the main panel.

No schema / migration / API / service changes — DeliverableAttachment
already had the stageDefinitionId field, the POST endpoint already
accepted it, and useAttachments already returned stageDefinition.
Pure UI work; rides on the existing query cache so the indicator
counts update instantly when the main panel or a sibling stage
popover writes.
2026-04-21 16:06:48 -04:00
DJP
553f1f8fa1 Assign + book hours in one shot from the stage card
Producers assigning work can now optionally book the artist's hours
in the same popover — no round-trip to the Resources tab.

Popover changes (AssignArtistPopover):
- Accepts an optional `jobNumber` prop. When provided, a "Also book
  hours" checkbox appears between the role selector and user search.
  Expanded state reveals a date input + 1/2/3/4/6/8h chip group.
  Collapsed by default so the common assign-only flow stays one-click.
- On user pick, the assign mutation fires first; on success, if the
  book-hours toggle was on we chain a ResourceBooking mutation. A
  booking failure keeps the assignment in place and surfaces a
  specific error toast ("Assigned, but booking failed") so producers
  aren't left wondering which half went wrong.

Plumbing:
- useCreateBookingAnyWeek() — booking mutation not bound to a
  specific week's query key. Blast-invalidates every ["bookings",*]
  query instead, since the deliverable detail page doesn't know
  which week the chosen date falls in.

Wiring:
- deliverable detail page passes `deliverable.project.omgJobNumber`
  as `jobNumber` — getDeliverable already includes project context
  in its response (added earlier for the info panel).
- Deliverables without an OMG job number keep the old assign-only UI.
2026-04-21 15:31:20 -04:00
DJP
d4bee0e8d3 Assignment notifications + workload approved-filter + My Work fixes
Four connected UX improvements in one commit:

1. Assignment creates a Notification row. notifyAssignment() was
   already written in notification-service but nothing called it.
   Wired into assignUserToStage right after the upsert — fires
   only on NEW assignments (not role updates), never self-notifies,
   non-blocking. The assignee sees it on /notifications with a
   link straight to the deliverable.

2. Workload drops approved stages from weekly totals. Previously
   terminal stages (APPROVED / DELIVERED / SKIPPED) stayed in the
   week they completed, making people look busier than they were.
   Now they drop the moment the stage goes terminal, matching the
   user's intuition ("if it's approved it shouldn't count as my
   workload any more").

3. My Work rows are clickable — each row is now a <Link> to the
   deliverable detail page. Hover state too.

4. My Work has a completed-window toggle. Pill group in the header:
   "Active only" (default) / "+ completed 1w" / 2w / 4w. Switches in
   APPROVED / DELIVERED / SKIPPED assignments whose completedDate
   falls inside the chosen window. No "all time" option — that list
   grows without bound.
2026-04-21 15:29:10 -04:00
DJP
827ed587bb Attachments panel — files + external links per deliverable
New first-class attachments concept on the deliverable detail page,
replacing the "stash a Figma URL in the notes" workaround. Two kinds
share one row:

- FILE   — uploaded via the existing /data/uploads volume, served
           through /api/uploads/deliverables/:id/:filename. 100MB cap.
           Images render a thumbnail inline; PDFs/docs get a file icon
           + download link. Blob cleaned up on delete.
- LINK   — external URL (Figma / Drive / Dropbox / ad-hoc). Figma URLs
           detected against the file/proto/design/board path pattern
           and get an in-line iframe preview via Figma's embed host.
           Other URLs open in a new tab with an external-link icon.

Schema:
- DeliverableAttachment model + migration. Cascade-deletes with the
  parent deliverable. Optional stageDefinitionId tag scopes an asset
  to a specific stage (null = whole deliverable).
- Extended serving route's MIME allowlist: PDF, Office docs, CSV,
  text, ZIP, AI/PSD. Was tight to video/image-only before.

Plumbing:
- attachment-service with listAttachments / createLink /
  createFile / delete. Visibility-scoped (assertProjectVisible via
  deliverable.projectId).
- POST /api/deliverables/:id/attachments — content-type discriminates
  multipart (file) vs JSON (link). DELETE at the nested route.
- useAttachments / useCreateLinkAttachment / useUploadFileAttachment /
  useDeleteAttachment hooks; invalidate ["attachments", deliverableId].

UI panel renders above Pipeline Stages so assets are always in sight.
Read-only for CLIENT_VIEWER; all other roles can upload/paste/delete.
2026-04-21 15:25:48 -04:00
DJP
c12e647546 Gate XLSX + per-stage notes + expanded deliverable info panel
1. /projects Import XLSX now behind the same isAdmin gate as New
   Project. Producers won't see either — bulk + manual intake are
   both admin-only escape hatches.

2. Per-stage notes. Backend was already wired (DeliverableStage.notes
   + updateStageSchema.notes + PATCH handler); just no UI.
   - New StageNotes component — inline editor per stage row. Empty
     state shows a "+ Add note" link; existing notes render as
     muted italic text, click anywhere on them to edit. Save on
     blur or ⌘/Ctrl+Enter, Esc to cancel.
   - useUpdateStageNotes hook for the PATCH.
   - Wired into each stage row on the deliverable detail page.

3. Deliverable info panel expanded + promoted above Pipeline Stages.
   - getDeliverable service now includes parent project context
     (id, name, projectCode, omgJobNumber, clientTeam) so the UI
     doesn't need a second round-trip.
   - Metadata grid always renders; empty fields show em-dash so
     producers can spot what's missing. Project row at the top
     surfaces project name, code, OMG #, client team. Deliverable
     notes are now shown as a block alongside the rest.
2026-04-21 14:49:07 -04:00
DJP
4fe65f2a61 Gate New Project / New Brief / Add Deliverable behind ADMIN role
Producers don't need to create these by hand — they'll arrive via the
three upstream webhooks. Manual creation is now an ADMIN-only escape
hatch (corrections, test data, etc.).

Plumbing:
- New GET /api/me endpoint exposes session fields (role, org, teams)
  to client components. Minimal — just what UI gates need.
- New useCurrentUser() + useIsAdmin() hooks in src/hooks/.

Gates:
- /projects: "New Project" hidden for non-ADMIN (Import XLSX left open)
- /briefs:   "New Brief"    hidden for non-ADMIN
- /projects/🆔 "Add Deliverable" hidden for non-ADMIN

Server-side permission model (PROJECT_CREATE on PRODUCER, etc.)
unchanged — if we want to tighten server-side too we can sweep
DEFAULT_PERMISSIONS in seed-dow.ts separately.
2026-04-21 14:37:45 -04:00
DJP
0ba7c2b464 Timeline: default filter to "all" + add missing statuses + team scope
Three fixes for an empty Production Timeline:

1. Default statusFilter flipped from ACTIVE → "all". Fresh imports
   land at PIPELINE status (brief-accepted), so ACTIVE-default hid
   every project on first load.
2. Status dropdown was missing PIPELINE and CANCELED — so even
   after you realised ACTIVE was wrong you couldn't pick PIPELINE.
   Added both.
3. /api/timeline was scoping by organizationId only, not ClientTeam
   visibility. Non-ADMIN users would have seen cross-team projects.
   AND-in visibleProjectsWhere(ctx) now — same pattern as the rest
   of the service layer.
2026-04-21 14:11:16 -04:00
DJP
4fde413aa9 Tune typography bump 1.5× → 1.2×
Previous 1.5× felt oversized on laptop screens. This commit:

1. Reverts the 1.5× sweep back to original sizes
2. Reapplies a gentler 1.2× multiplier — both in the @theme text
   scale (text-xs → 14.4px, text-base → 19.2px, etc.) and via the
   arbitrary-size sweep across 420 tokens in 81 files
3. .label-upper utility bumped 10px → 12px

Net effect: everything is ~20% larger than the pre-bump baseline,
~20% smaller than the previous 1.5× state.

Approach avoids compound-multiplication rounding drift: a true
"undo then reapply" rather than scaling the already-scaled values.
2026-04-21 13:57:32 -04:00
DJP
ffd91cea04 Bump typography ~1.5× across the app
Producers were reading the UI at a squint. Two pieces:

1. @theme in globals.css now overrides every --text-* token (xs
   through 7xl) to 1.5× the Tailwind v4 default, with matching
   line-heights. This covers everything using the named classes
   (text-xs / text-sm / text-base / …) without touching markup.
   Also bumped .label-upper from 10px → 15px.

2. Scripted sweep of every arbitrary text-[Npx], fontSize={N}, and
   inline `fontSize: N` (recharts ticks) — 421 occurrences across
   82 files. Each N was rounded to round(N × 1.5). Idempotency is
   NOT preserved; running the sweep twice would scale twice.

Script lives at /tmp/scale-text-sizes.mjs for reference — not
committed. If we need to roll back, `git revert` this commit is
safe (pure display change, no schema/service impact).
2026-04-21 13:42:54 -04:00
DJP
f2cdd8bd4c Fix rework drag: reset intermediate stages so the card actually moves
The rework path was "working" on the server (target went IN_PROGRESS,
success toast fired) but the card stayed in the source column. Root
cause: applyRework() only touched the target row, leaving the source
stage's status untouched. The board's currentStage() picker takes the
highest-order ACTIVE stage, so with both the target (e.g., In Progress
Creative, order 5) and the source (e.g., Client Feedback, order 7)
both IN_PROGRESS, the source won and the card never moved.

Fix: on rework, reset every stage strictly between target.order and
source.order (inclusive of source) to NOT_STARTED if they were
APPROVED/IN_PROGRESS/IN_REVIEW/CHANGES_REQUESTED/DELIVERED. This
matches the real-world semantic too — any approvals downstream of the
rework point are approving content that has now gone back upstream
for fixes, so they need to be re-earned.

SKIPPED stages are preserved (a conscious skip shouldn't be silently
un-skipped by a rework). Both stage writes happen in one transaction.
2026-04-21 13:38:22 -04:00
DJP
03cd99b56c Add INTEGRATION.md — handover spec for upstream teams
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.
2026-04-21 13:29:44 -04:00
DJP
f89fb73aff Pipeline editor: rework paths (pass/fail loops) now configurable in the browser
Admins can draw backward transitions on the same ReactFlow canvas
that already handles forward dependencies. With this, the pipeline
editor is a full workflow builder — new pipelines for new clients
can be assembled in the browser without code changes.

How it works:
- "Workflow Graph" panel now has a mode toggle: Dependencies /
  Rework paths. Both edge types always render — the toggle just
  scopes what dragging between stages creates.
- Forward (solid) and rework (dashed red) edges coexist on the
  canvas; arrows point source→target in both cases.
- Drag validation: rework drops where target.order >= source.order
  are rejected client-side (silent — a round-trip error would just
  confirm the same thing).
- Click the × on any edge to delete it.

Plumbing:
- pipeline-template-service: addReworkPath / removeReworkPath with
  same-pipeline + not-self + strictly-backward checks.
  getPipelineTemplate includes reworkFrom + reworkTo so the canvas
  renders reworks alongside dependencies without a second fetch.
- POST/DELETE /api/pipelines/:id/reworks
- useAddRework / useRemoveRework hooks
- New ReworkEdge component — dashed, red, curvier arc to keep
  pass/fail visually distinct from the sequential forward flow
2026-04-21 13:19:43 -04:00
DJP
9ff0f03a4d Board drag-and-drop with forward/rework pipeline rules
Producers can now drag cards on both boards. Three distinct write
paths, each with validation appropriate to what it's moving:

Projects board (status lens only):
- Drop updates Project.status via PATCH /api/projects/:id.
- Stage lens stays read-only — Project.stage is derived from the
  dominant deliverable stage, there's no single write target.

Deliverables board (status lens):
- Drop updates Deliverable.status directly.

Deliverables board (stage lens) — the interesting one:
- Drop hits new POST /api/deliverables/:id/transition endpoint,
  validated by new stage-transition-service.
- Forward transitions: current → APPROVED, optional intermediates
  → SKIPPED, target → IN_PROGRESS. If a REQUIRED intermediate
  isn't already done, the drop is rejected ("walk through it
  first") — toast shows the blocking stage name.
- Rework (backward) transitions: only allowed if the pipeline
  template declares an explicit rework path from current → target.
  Otherwise rejected. No cross-pipeline drops possible because the
  target slug is looked up inside the deliverable's own pipeline.

Schema:
- New PipelineStageRework self-join on PipelineStageDefinition —
  per-template declaration of "from stage X you can push back to
  stage Y". Migration 20260423000000_pipeline_stage_reworks.
- Seed populates Dow's canonical rework paths: Client Review
  (Copy) → Copywriter, Internal Review → In Progress Creative,
  Client Feedback → In Progress Creative, Final Approval → In
  Progress Creative, Completed → In Progress Creative.

Pipeline template editor UI (where producers would add their own
rework paths) is deferred; the seed covers Dow out of the box.

Hooks: added useUpdateProjectById, useUpdateDeliverableById,
useTransitionDeliverable — all take ids at mutation time so the
boards don't need one hook per card.
2026-04-21 12:45:17 -04:00
DJP
e9f8fffdcc Fix board stage columns + add board view to Deliverables
Two related bugs + one feature the user flagged:

1. **dominantStage picked Canceled for every fresh project**
   The old logic included BLOCKED in "in-flight" and picked the
   highest-order match. Dow's pipeline puts "On Hold" (order 10)
   and "Canceled" (order 11) as parking stages with NO prereqs —
   so on fresh deliverables they start as NOT_STARTED, which made
   "Canceled" the highest-order in-flight stage for every deliverable.
   Result: board view only rendered a Canceled column.

   Fix — same two-step pick in both project-service.ts and
   deliverables/page.tsx currentStage():
     1) highest-order ACTIVE stage (IN_PROGRESS/IN_REVIEW/CHANGES_REQUESTED)
     2) else lowest-order NOT_STARTED (next-up)
   BLOCKED is skipped entirely — it means "prereqs not done", not
   "where work is". The lowest-order NOT_STARTED rule naturally
   keeps parking stages out of the dominant pick unless they're
   actually being worked.

2. **Board hid empty stage columns**
   In stage-grouped mode the board only rendered columns for
   stages seen in the data, so when the dominantStage bug bucketed
   everything into Canceled, all other columns disappeared.
   ProjectBoard + the new DeliverableBoard now accept a
   pipelineStages prop (from the default pipeline template) and
   render every stage as a column in canonical order, empty or not.

3. **Deliverables page: Board view**
   New component src/components/deliverables/deliverable-board.tsx.
   Grid/Board toggle in the header, group-by selector (Stage/Status)
   next to the filters. Cards show OMG #, priority dot, team, name,
   project, primary assignee, deadline. Clicking a card navigates
   to the deliverable detail page.
2026-04-21 12:28:11 -04:00
DJP
958de5f3a9 Add Briefs intake + all three upstream webhooks
Briefs are pre-project requests. Three intake paths land in one place:
  1. Manual  — "New Brief" dialog on /briefs
  2. REST    — POST /api/briefs (auth'd)
  3. Webhook — POST /api/webhooks/briefs (HMAC-signed)

Once triaged, "Promote to Project" flips Brief.status → CONVERTED,
creates the Project, and links them via convertedProjectId so the
audit trail stays intact.

Schema:
- New BriefStatus enum + Brief model, indexed on org/status/team
- Unique on (organizationId, externalId) so webhook replays are
  idempotent — same upstream id = update, not insert
- Migration 20260422000000_briefs, hand-written SQL

Webhooks — now three total, each with its own secret and header:
- /api/webhooks/omg           (projects — existing, unchanged)
- /api/webhooks/deliverables  (NEW — keyed on project OMG # + name)
- /api/webhooks/briefs        (NEW — keyed on externalId)

Extracted a shared HMAC verifier at src/lib/webhooks/hmac.ts so the
two new routes don't copy-paste the crypto code from the OMG route.
Deliverables webhook looks up the parent project by OMG job number
(the canonical key from the projects webhook); returns 404 with a
hint if the project hasn't been created yet. Brief webhook source
records "webhook:<system>" so we can tell where briefs come from.

UI:
- /briefs page: filterable/searchable table, inline status dropdown
  per row, New Brief dialog, Promote to Project dialog
- Sidebar nav entry for Briefs above Projects

Env: added DELIVERABLE_WEBHOOK_SECRET / ALLOW_INSECURE and
BRIEF_WEBHOOK_SECRET / ALLOW_INSECURE alongside the existing OMG
pair in .env.example.
2026-04-21 12:05:47 -04:00
DJP
d065f4b56f Projects: add Board view with Status/Stage grouping
New ProjectBoard component — Kanban columns of project cards with a
view toggle in the header. Two grouping modes:

- Group by: Status — fixed columns (Pipeline / Active / On Hold /
  Completed / Canceled / Archived). Empty columns still render so
  producers can see "nothing here" at a glance.
- Group by: Pipeline Stage — columns derived from the dominant
  in-flight stage already computed server-side
  (pipelineProgress.dominantStage). Ordered by stage.order so the
  flow reads Pipeline → Completed.

Cards show OMG #, priority dot, team badge, name, deliverable
count, deadline (red if overdue), a progress bar against completed
deliverables, and the dominant stage label. Click navigates to the
project detail page.

Read-only for now. Drag-to-move on the status lens would be
straightforward; the stage lens can't be dragged since a project's
"stage" is derived from its deliverables, not a project field.
2026-04-21 11:57:07 -04:00
DJP
12f8817108 Compact stacked stage list + fold bookings into workload
Deliverable stages:
- Killed the broken "Review" link that pointed to a route removed in
  the initial HP→Dow port (was producing 404s on click)
- Rebuilt the 2-col grid of big Card components as a tight vertical
  stack of rows. Status badge pinned right, dates + assignees +
  action buttons inline on a single secondary row, left accent
  stripe preserved for quick status scanning. Roughly 3× the stages
  fit on screen now.

Workload × Resources:
- Resources page's ResourceBooking hours were invisible on the
  Workload page — the service only counted StageAssignments.
- getWorkloadData now runs a single bookings query for the full
  horizon, aggregates by (user, week-start) via a Map, and exposes
  bookedHours + weeklyCapacityHours per week.
- A week is overloaded if EITHER assignment count exceeds daily cap
  OR booked hours blow past weekly cap. Capacity cell shows both
  numbers (count, +Nh booked) and uses max(assignmentUtil,
  bookingUtil) for the colour tint so booking-only weeks still
  render as loaded.
2026-04-21 11:54:30 -04:00
DJP
405da7d2f8 Add cross-project Deliverables view + role/team filters on Resources
Deliverables view (new /deliverables):
- listAllDeliverables service uses visibleDeliverablesWhere so the
  flat list respects per-team scoping automatically
- GET /api/deliverables returns the flat list joined with project +
  stages + assignments
- useAllDeliverables hook + typed AllDeliverableRow
- New page: searchable/sortable table, filter by project, stage,
  team, status, priority. Current-stage column uses the same
  "highest-order in-flight stage" logic as the Projects grid so
  both views agree on what counts as "where this deliverable is"
- Sidebar: new "Deliverables" nav entry between Projects and My Work

Resources page filters:
- Extended listUsers to include homePod + clientTeams so the page
  can filter/group without an extra round-trip
- Added Role + Team filters, Group-by selector (Team / Role / Pod /
  Department), Sort-by selector. Default is Group by Team + Sort by
  Name — matches how the roster is organised on the team-list xlsx
- primaryTeam() picks the isPrimary membership so Shared users
  (members of multiple teams) land in one bucket when grouped
2026-04-21 11:13:40 -04:00
DJP
985f8effbc Typography: swap Inter → Public Sans globally
Three touchpoints:
- layout.tsx: import Public_Sans from next/font/google instead of
  Inter, same --font-sans CSS variable so the rest of the app picks
  it up without changes
- globals.css: --font-sans stack heads with "Public Sans"
- annotation-renderer.tsx: SVG text nodes had a hardcoded
  "Montserrat, Inter, sans-serif" family — updated to match
2026-04-21 10:52:19 -04:00
DJP
7ab7205fd5 Theme: swap Oliver green → Dow navy
Previously the primary brand color was Oliver's forest green
(#08402c light / #0fa968 dark) — left over from the HP fork. Now:

- --primary, --ring, --chart-1 → #002B5C (Dow navy) in light mode;
  #5D9AD6 (lighter Dow blue, for contrast on dark bg) in dark mode
- --status-approved moves off the Oliver brand hex onto standard
  success green (#16A34A) so approval badges still read as "success"
  without being brand-coded
- Print overrides updated to match

Kept: semantic/universal greens across the app (approval badges,
emerald "healthy capacity" heatmap, CheckCircle success icons, role
colors) — those aren't brand-identity uses.
2026-04-21 10:50:21 -04:00
DJP
77151bb53e Skeletons pulse in Dow navy + fix stretched logo
- Skeleton.tsx was inheriting bg-accent from the Oliver/HP theme
  (#ee5540 coral), so every loading state read as a red error.
  Switched to bg-[#002B5C]/15 — soft Dow navy tint, matches the
  logo background.
- Logo PNG is 400x48 (8.33:1) rendered at h-7 w-auto, giving an
  intrinsic 233px width. Inside a flex parent narrower than that,
  Tailwind preflight's max-width:100% kicked in while h-7 pinned
  the height, so the image was being stretched horizontally.
  Adding object-contain + max-w-full lets the image scale
  proportionally instead of distorting.
2026-04-21 10:43:41 -04:00
DJP
559d8766cd Seed: real Dow/Oliver roster (20 users) replaces placeholders
Sourced from Dow Jones_Team List_042027.xlsx (2026-04-21 handover).

- 20 users across Performance (9), Events (6), Shared (5)
- Roles mapped from Dow titles: Ops/Business/Creative Director → ADMIN;
  Sr/Project Manager + Copy Lead + QC Manager → PRODUCER; Designers +
  Motion + Copywriters → ARTIST
- ClientTeam memberships created per roster: performance/events users
  get their one team; Shared users get both (so non-admin cross-team
  roles like Copy Lead-ACD and QC Content Manager can actually see
  work across both pods)
- Pods realigned team-first: performance-pod / events-pod /
  leadership-pod (replaces the Sergio/Deborah/Shared placeholders)
- department field stores the Dow title verbatim for display
2026-04-21 09:46:45 -04:00
DJP
04c18ab263 Deploy script: persist ports to .env + auto-seed on empty DB
Two recurring deploy headaches fixed:
- Port collisions on rerun: find_free_port picks a new port, but without
  saving it, subsequent `docker compose up` calls ignore it and collide
  again. Now written back to .env so Compose picks them up.
- Manual `npm run db:seed` after every fresh deploy. Now auto-runs iff
  the organizations table is empty (first deploy or clean-slate),
  otherwise skips.

Result: `./deploy.sh` is now a single, idempotent command.
2026-04-21 09:37:02 -04:00
DJP
a0ebf4a1bb Project form actually Dow-shaped, Pipeline Stage column, bigger job picker
Three fixes that should have been in earlier commits but weren't:

1) project-form-dialog.tsx — REWRITTEN for real this time. Earlier Write
   call in commit 18ae429 claimed success but didn't persist, so every
   HP placeholder was still live in prod. Verified: zero HP/Envy/NPI/
   Form Factor/Business Unit strings remain; 10 new Dow-specific
   bindings (clientTeam, omgJobNumber, DOW_CATEGORIES, useClientTeams).
   Three tabs (Details / Dates / References) with ClientTeam selector,
   OMG Job Number field, Risk/Priority labels, Dow category dropdown,
   full Dow ProjectStatus enum including PIPELINE and CANCELED. Labels
   point at the exact Dow XLSX column names so producers recognise
   the mapping.

2) Projects page: new Pipeline Stage column.
   listProjects() now aggregates pipeline progress per project: for
   each deliverable it finds its highest-order in-flight stage (the
   where-it-currently-is); the dominant stage across all deliverables
   is the one shown on the row. Deliverables whose every stage is
   APPROVED/DELIVERED/SKIPPED count as completed. New
   PipelineProgressCell renders 'Stage Name - N/Total' plus a mini
   progress bar. Green 'Done' pill when every deliverable is past
   the pipeline. Column is sortable by stage order.

3) Resources popover: replaced the dense pill-strip with a proper
   vertical list of available jobs. Each row shows the color dot +
   the OMG number (mono, colored) + the project name. Scrollable,
   sticky header with a count, full-width clickable. Double-click
   a row to commit. Empty states explain where to upload the XLSX
   or how to enter freeform. Popover widened 280->340px.

tsc --noEmit zero errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:03:51 -04:00
DJP
e439ea09db Fix missed .mjs db:seed script + Dow navy bg behind logo
Two smalls:

1) package.json db:seed still said seed-dow.cjs from the earlier
   CJS→ESM rename — file bump missed it because I hadn't read the file
   in that tool pass. With the Dockerfile now producing .mjs the seed
   would have failed again for a different, dumber reason. Fixed.

2) Dow Jones wordmark is white on transparent, so it vanished into the
   sidebar's light background. Added bg-[#002B5C] (Dow Jones brand
   navy) to the logo header in both the desktop sidebar and the mobile
   sheet. Now the logo actually reads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 08:56:53 -04:00
DJP
29e76b7e33 Seed bundle: switch CJS→ESM so import.meta.url works
Prisma's generated client calls fileURLToPath(import.meta.url) to locate
its query engine binary. In CJS output that's undefined, and the require
of the seed bundle blew up before running a line of actual seed code.

ESM output (.mjs) runs natively on Node 22, import.meta.url resolves
correctly, everything else about the bundle (external npm packages,
inlined generated client) stays the same.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 08:56:00 -04:00
DJP
670692f46e Dockerfile: rm -rf the broken pg chain before reinstall
Root cause nailed down: the Next.js standalone tree ships package
manifests for the pg driver chain (postgres-array, postgres-bytea etc.)
without the corresponding source files. npm's 'version already
satisfies, skipping' logic fires on every subsequent install — --force,
--no-save, explicit pg@8, all of them. The manifest is there, npm says
done.

Fix: rm -rf every package in the chain first, then fresh install. Plus
a fail-fast check at the end (test -f postgres-array/index.js) so a
silent regression surfaces as FATAL in the build log instead of hours
later at seed time.

Sorry for the runaround. This should've been the first move.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:59:40 -04:00
DJP
a66411ea53 db:seed: drop tsx fallback + stderr silencer so errors surface
The '2>/dev/null || tsx' fallback was hiding the real error from the
compiled .cjs (which is what we want to run) and then the fallback
printed 'tsx: not found' — gaslight-grade diagnostics.

Prod image has node + the compiled .cjs, so just run that. If anyone
needs to run the raw .ts in dev, 'npx tsx prisma/seed-dow.ts' still
works ad hoc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:57:04 -04:00