Commit graph

179 commits

Author SHA1 Message Date
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
DJP
d5467348a7 Precompile seed to CJS + plain <img> for the logo
Two runtime-surface fixes surfacing from the optical-dev deploy.

1) Seed's `postgres-array/index.js` kept missing even after --no-cache
   rebuilds. Root cause: the Next.js standalone output traces package
   manifests aggressively but sometimes leaves the `main` file body
   out when the package isn't reachable from the app's import graph
   (postgres-array is a transitive of pg, only reached via the seed
   script, not the app runtime). Our `npm install --no-save
   @prisma/adapter-pg` at build time then saw the partial install and
   short-circuited. Runtime tsx resolution then blew up on `require
   postgres-array`.

   Fixed two ways, layered:
   a) In the builder stage, after `npm run build`, esbuild the seed
      into a self-contained CJS bundle at prisma/seed-dow.cjs.
      `--packages=external` keeps npm packages as runtime require()s
      so native .node files (via @prisma/client) work, but everything
      else is bundled so the seed no longer depends on runtime module
      resolution in the fragile standalone tree.
   b) In the runner stage, `npm install --no-save --force` (plus
      explicit `pg@8` which pulls its postgres-* deps cleanly)
      overwrites any partial packages the standalone shipped with.
      Belt-and-braces with the bundled seed.
   c) package.json `db:seed` now prefers `node prisma/seed-dow.cjs`
      and falls back to `tsx prisma/seed-dow.ts` if the .cjs isn't
      there (e.g. when running the seed from a dev box where no build
      happened). Both paths produce identical output.

   No more runtime tsx install — dropped `npm install -g tsx@4` from
   the runner image. That was always a workaround; the bundle is the
   actual fix.

2) Sidebar logo not rendering — Next.js's `<Image>` with basePath +
   the standalone image optimizer is finicky; the image file IS at
   /app/public/navbar-logo.png in the container but Next's
   `/_next/image?url=...` pipeline was returning 404 for it. Swapped
   to a plain `<img>` (with the eslint-disable comment so the rule
   doesn't whine). The file is ~4KB, image optimization added
   nothing. Desktop + mobile sidebars both use the plain tag now.

Verified: tsc --noEmit ✓ zero errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:46:15 -04:00
DJP
18ae429924 Dow-ify project form + seed placeholder roster + Resource Manager + docs
You flagged three concrete gaps after the deploy went live — all
addressed in this commit, plus the API + how-to docs you asked for.

A) Create Project dialog was still HP-centric
   Placeholders like "HP Envy x360 Renders" / "HP-2026-001" / "NPI /
   Refresh" / "Form Factor" etc. bore no relation to Dow's actual XLSX
   columns and the form had no ClientTeam selector — so any
   admin-created project was orphaned from the visibility layer.
   - src/lib/validators/project.ts: added clientTeamId + omgJobNumber;
     status enum now includes PIPELINE and CANCELED
   - src/components/projects/project-form-dialog.tsx: rewritten around
     the Dow XLSX schema. Three tabs (Details / Dates / References)
     instead of four. Placeholders reference real Dow values
     (Celena / Yzabella etc. for Owner, 2337959 for OMG #, Brand / Events
     etc. for Team, Copywriting/Display/... for Category). ClientTeam
     selector populated from /api/client-teams with a "no teams — add
     one in Settings" fallback. Category is a typed enum dropdown with
     the 8 XLSX values. Risk/Priority wording mirrors the XLSX labels
     (Priority = URGENT). Dropped HP-only fields from the UI
     (formFactor, codeName, npiOrRefresh, businessUnit placeholder,
     agency, Financial tab, Workfront ID placeholder). Legacy fields
     are still in the Zod schema for back-compat but not rendered.

B) Users invisible because only the admin was seeded
   The plan flagged "real Dow/Oliver roster — open question" and we
   never got the list, so the seed only created admin@dowjones.com.
   prisma/seed-dow.ts now also creates the 9 placeholder resources
   from the Resources.html prototype (Alice Chen, Ben Marsh, Cara Wu,
   Dan Koch, Eva Stone, Frank Osei, Grace Lee, Hiro Tanaka, Isla Reeve),
   distributed round-robin across the three placeholder pods. Each has
   role + department + maxCapacity set but no passwordHash, so they
   show up in the UI immediately but can't log in until an admin
   invites them via Settings → Team (which issues a reset link).
   Swap for the real roster whenever Zia delivers it — the emails are
   @example.com so they're safe to delete.

C) Resource Manager page (matching Resources.html)
   New capacity planner UI — daily hours-per-job grid.
   - Schema: new ResourceBooking model { userId, date, jobNumber,
     hours, note, organizationId, createdById }. Migration at
     prisma/migrations/20260421000000_resource_bookings.
   - Validator (src/lib/validators/booking.ts): create + list schemas
     with date-only coercion.
   - Service (src/lib/services/booking-service.ts): week window
     helpers, create/list/delete + known-job-numbers lookup for the
     popover autocomplete.
   - API: GET/POST /api/resources/bookings, DELETE
     /api/resources/bookings/[id], GET /api/resources/job-numbers.
     Writes gated to ADMIN + PRODUCER; reads open to any signed-in
     member of the org (capacity view is a shared studio-level thing,
     not per-team visibility).
   - Hook (src/hooks/use-bookings.ts) with TanStack Query wiring +
     week-scoped cache keys.
   - Page (src/app/(app)/resources/page.tsx) ports the Resources.html
     design to the app's Tailwind + shadcn primitives: Resource × Day
     grid grouped by department, week navigator, click-to-assign
     popover with job-number autocomplete + hour chips (1/2/3/4/6/8 +
     custom), capacity bar per cell, week total column with over-cap
     warning, collapsible role bands. Matches the prototype's
     color-hashed job chips so the same job number gets a consistent
     color across the grid.
   - Sidebar nav: added "Resources" entry next to Workload.

D) Docs — full README + API reference + how-to
   - API.md: complete REST + webhook reference. Three auth modes
     documented (session cookie / X-API-Key / OMG HMAC). XLSX upload
     header map with the Dow XLSX column correspondences. OMG webhook
     has the speculative payload shape + a working bash example that
     signs + sends a request. Common flows at the bottom: bootstrap
     from zero, OMG publishes a status change, update a job from an
     external script.
   - HOWTO.md: end-to-end runbook. Mental model, local dev, prod
     deploy pointers, first-login ritual, add-users flow (UI + API),
     client teams + pods config, XLSX ingest (UI + curl + idempotency
     notes), OMG webhook wiring (secret gen through verification),
     producer daily workflow, client-viewer experience, resource
     planning walk-through, RBAC matrix, common-problems table, and
     "change the model" pointer map for future edits.
   - README.md: top intro now points at API.md / HOWTO.md / DEPLOY.md.

Verified: npx tsc --noEmit ✓ zero errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:31:19 -04:00
DJP
df7ddbfb0d Dockerfile: install tsx globally so it stops disappearing
`npm install --no-save tsx` in the runner stage was reporting success
but the tsx binary kept vanishing from node_modules/.bin after the
Next.js standalone node_modules reshuffled its tree. The seed then
failed with "sh: tsx: not found" even after a clean rebuild.

Install tsx globally instead (npm install -g tsx@4 → /usr/local/bin/tsx),
which is PATH-resolvable regardless of any local node_modules churn.

Other seed-adjacent deps stay local (prisma CLI, dotenv, adapter-pg,
bcryptjs) because the seed script explicitly imports them and wants
them resolved from the app's local node_modules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:33:55 -04:00
DJP
096f0cbe93 deploy.sh: write Apache Include to sites-enabled when it isn't a symlink
Standard Ubuntu Apache uses `a2ensite` to create a symlink from
sites-enabled → sites-available. On optical-dev.oliver.solutions the
sites-enabled file is a separately-managed real file (not a symlink),
so our sed-into-sites-available edited one file while Apache was loading
from another. The deploy reported "Include already present" or "Added
Include" but Apache kept 404-ing because it was reading a different
file that had no such Include.

Detect which case we're in and edit the right one:
- symlink → edit sites-available (edit propagates through the symlink)
- separate file → edit sites-enabled directly, with a warning so ops
  knows the setup drifted from a2ensite convention

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:59:12 -04:00
DJP
2b18c99296 Fix stuck deploy: seed deps missing from prod image + health check too strict
Caught on the first real deploy to optical-dev. Two separate bugs.

Dockerfile — runner stage was missing tsx + @prisma/adapter-pg + bcryptjs
   The Next.js standalone bundle covers the app, but prisma/seed-dow.ts
   is a separate .ts file executed via tsx (not bundled). Runner only
   explicitly installed prisma + dotenv, so `npm run db:seed` failed with
   "sh: tsx: not found" and deploys couldn't run the one-time seed.
   → Added tsx, @prisma/adapter-pg (seed uses PrismaPg directly), and
     bcryptjs (seed hashes the admin's temp password) to the
     `npm install --no-save` line in the runner stage. Adds ~15 MB to
     the final image — worth it for a working seed path.

/api/health was 503 pre-seed, which made deploy.sh unwillingly block itself
   The probe in deploy.sh uses `curl -sf` and treats any non-2xx as
   "not ready". The health endpoint flipped the entire `healthy` flag to
   false when `organizations` or `pipeline_templates` counted zero —
   meaning a freshly-migrated-but-not-yet-seeded app was classified as
   unhealthy, deploy.sh gave up at Step 6, and we never got to Step 7
   (Apache config) or Step 8 (UFW). End result: the URL 404'd because
   Apache wasn't proxying anything to the container.
   → Split liveness from readiness:
     - GET /api/health (default) — DB reachable, pgvector installed,
       AUTH_SECRET set, DEV_BYPASS off. Empty tables are reported as
       "warn" but do NOT 503. This is what deploy.sh waits on.
     - GET /api/health?strict=1 — same checks PLUS org + templates
       present. Use post-seed to verify everything landed.
   - Added a "mode" field ("liveness" | "strict") so which mode was
     used is visible in the response.
   - Pre-seed content-level checks now return status: "warn" with a
     hint to run `npm run db:seed`, instead of hard-failing.

Net effect for a fresh deploy:
  ./deploy.sh → builds, runs migrations, reports healthy once DB +
  env are good, configures Apache, DONE. Then you can
  `docker compose -p dow-prod-tracker exec app npm run db:seed`
  at your leisure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:45:44 -04:00
DJP
870128c16e DEPLOY.md: update paths from /srv to /opt
The actual deploy on optical-dev.oliver.solutions lives at
/opt/dow-prod-tracker — standard location for third-party apps on
Debian/Ubuntu. The docs said /srv (my arbitrary choice). Both work;
docs should match reality.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:37:18 -04:00
DJP
be46089569 Fix Next.js 16 build: wrap /change-password useSearchParams in Suspense
The static prerender step bails on a CSR-only component that reads
useSearchParams() outside a Suspense boundary. Caught by the first real
production build (Turbopack/standalone output).

Split ChangePasswordPage into an outer Suspense shell (default export)
and an inner ChangePasswordForm that owns the useSearchParams() call.
Fallback is null — the shell renders for ~1ms before the client hydrates
the form, invisible.

No behavior change. No other auth pages use useSearchParams.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 20:25:04 -04:00
DJP
7e7ef7b7c1 deploy.sh: auto-detect free host ports, render Apache conf per-deploy
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>
2026-04-20 19:59:30 -04:00
DJP
90a5031de2 Excel-grid /projects page — Dow-shaped table view
The card-grid view that came over from HP was never going to feel like a
Planner/Excel replacement. This rewrites /projects as a dense sortable
table that mirrors the Dow Studio Tracker XLSX column-for-column.

Columns (every one sortable, click header to toggle asc/desc):
  Owner · Risk · OMG # · Team · Status · Category · Project Name
  · Brief Accepted · Deadline · # Deliverables

Data maps straight off the upsertProjectFromDow() fields — what the XLSX
importer and the OMG webhook both write — so what you see in the grid
matches what the Dow team sees in the spreadsheet.

Filters (top bar):
- Search box — matches name, project code, OMG #, owner, category, notes
- Team dropdown — sourced from /api/client-teams (Brand / Events / B2B /
  Content / Briefing Team / Performance + any custom-added teams)
- Status dropdown — PIPELINE / ACTIVE / ON_HOLD / COMPLETED / CANCELED /
  ARCHIVED (the Dow enum)
- Live row count ("26 of 42 projects")

Affordances:
- Overdue deadlines render bold red with a warning icon (dueDate < now
  and status isn't terminal)
- Row hover reveals open-project / delete icons to keep the grid dense
  when idle
- Export CSV button — one-click dump of the filtered rows in XLSX
  column order, so the team still has the spreadsheet escape hatch
  while they migrate off Excel
- Client-side sort + filter because the dataset is bounded (one tenant,
  one row per project); server pagination is premature

Hides noise: Client Contact (PII, per the ingest policy) and Status
Details (long freeform text, better on detail page).

Verified: tsc --noEmit ✓; GET /projects renders 200 locally against the
real imported Dow tracker data (18 projects, 5 teams).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:50:58 -04:00
DJP
fe3f91c7ef Smoke-test fixes: routing collision + XLSX parser + defaults
Actually ran this for the first time. Three real bugs + two polish items.

1. NextAuth catch-all was eating local-auth routes
   src/app/api/auth/[...nextauth]/route.ts is a catch-all that claims
   everything under /api/auth/*. When AUTH_SECRET is set (i.e. outside
   of DEV_BYPASS_AUTH), NextAuth's handlers absorbed my static
   /api/auth/login, /api/auth/change-password etc. routes and returned
   404 for them.
   → Moved to /api/local-auth/*. Updated all four client pages to
   match. Added /api/local-auth to the middleware's authn-bypass
   allow-list alongside /api/auth.

2. XLSX header matcher too greedy on "team"
   The HEADER_MATCHERS entry for clientTeamRaw was ["team"], and
   findIndex used substring match. That matched "Creative Team Member
   Deliverable is Assigned to" (assignee column) BEFORE the literal
   "Team" column. Result: client-team values on imported projects were
   the assignee names ("gabrielle", "matt", "sergio").
   → Two-pass buildColumnMap: exact equality first (claims the
   literal "Team" cell for clientTeamRaw), substring fallback second
   (handles the verbose "Creative Team Member…" header for assignee).
   Already-claimed columns are excluded from subsequent passes.

3. exceljs hyperlink cells not unwrapped
   Project Name cells in the Dow tracker are a mix of plain strings
   (for rows Dow edited manually) and exceljs hyperlink objects
   (rows auto-linked to the OMG brief — shape `{ text, hyperlink }`).
   The old extractColumns only unwrapped richText and formula.result;
   hyperlink objects fell through and Zod rejected them with
   "projectName: Invalid input". 24 of 27 rows from the real XLSX
   failed with this before; now 26/27 pass (the 1 remaining error is
   a genuinely missing omgNumber, correctly flagged).
   → Extracted unwrapCell() that handles hyperlink, richText, formula,
   error, and Date cells.

4. DEV_BYPASS_AUTH defaulted to "true" in .env.example
   Anyone copying .env.example verbatim got a mock session pointing at
   the HP-era "dev-user-001" which doesn't exist in the Dow DB,
   causing mysterious P2025 errors on user.update. Also leaves the app
   wide open — nobody's auth is actually checked.
   → Default to "false" in .env.example with a DANGEROUS warning.

5. layout.tsx metadata description still said "HP CG department"
   → Fixed to "the Dow Jones studio".

Verified end-to-end on a fresh local DB:
- Login as seeded admin ✓
- Forced password change on first login ✓
- XLSX import: 27 rows → 26 created, 1 error (missing omg number) ✓
- 267 deliverables across 5 client teams ✓
- Invited a CLIENT_VIEWER, assigned to Brand team only ✓
- Brand tester sees 1 project; admin sees 18 ✓
- Brand tester gets 403 on POST /api/projects ✓

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:47:22 -04:00
DJP
a4107ae23d Phase 7+8: logo swap + deploy README
Logo:
- src/components/layout/sidebar.tsx — replaced text wordmark with the Dow
  navbar-logo.png in both desktop (collapsed-aware) and mobile sidebar
  variants. Logo links to /dashboard. Sized at h-7 (28px) — fits the
  400×48 source aspect ratio comfortably.

Deploy docs:
- DEPLOY.md — focused deployment guide for optical-dev.oliver.solutions.
  Highlights the CLAUDE.md shared-server safety rules (compose `name:`
  field + `-p` flag), env var checklist, first-time setup, update flow,
  the seven-step verification list, rollback, and common-issue triage.
  This is the doc you hand a new ops person along with the deploy.sh.
- README.md — top intro rewritten for the actual Dow product (Excel/
  Planner replacement, 11-stage pipeline, OMG + XLSX ingest, per-team
  visibility) instead of the inherited HP CG copy. Points at DEPLOY.md.

Verified: tsc --noEmit ✓ zero errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:27:31 -04:00
DJP
4361d4cd2a Phase 6e: ClientTeam + Pod CRUD — settings pages and APIs
Until now the only way to set up Dow's six client teams + production pods
was via the seed script. Now admins manage them from the Settings UI.

Validators (Zod):
- src/lib/validators/client-team.ts: create/update/membership schemas;
  slug regex enforced (lowercase + dashes only — keeps it stable for the
  XLSX/webhook ingest path which resolves teams by slug).
- src/lib/validators/pod.ts: create/update + setHomePod schemas.

Services:
- src/lib/services/client-team-service.ts: list/create/update/delete +
  addMember/removeMember. delete blocks if the team still has projects
  (forces an explicit move first). Auto-derives slug from name when not
  provided.
- src/lib/services/pod-service.ts: list/create/update/delete + setUserHomePod.
  delete is non-cascading on members — sets User.homePodId=null instead
  of deleting people. Lead-user assignment is org-scope-validated.

API routes (gated by new permissions CLIENT_TEAM_MANAGE / POD_MANAGE
seeded for ADMIN in Phase 3):
- GET/POST  /api/client-teams
- PATCH/DELETE  /api/client-teams/[teamId]
- POST/DELETE  /api/client-teams/[teamId]/members
- GET/POST  /api/pods
- PATCH/DELETE  /api/pods/[podId]
- POST/DELETE  /api/pods/[podId]/members

GET endpoints are open to any signed-in user — they need the lists for
filter dropdowns and to know their own team. Project-row visibility is
still enforced via Phase 2's visibility helpers, untouched.

Hooks:
- src/hooks/use-client-teams.ts and src/hooks/use-pods.ts — TanStack
  Query wrappers with cache invalidation on mutations.

Settings pages:
- src/app/(app)/settings/client-teams/page.tsx — create teams, manage
  memberships, see project counts. Hides external (CLIENT_VIEWER) users
  with a "client" badge so admins know who's who.
- src/app/(app)/settings/pods/page.tsx — create pods, set lead, add/remove
  members. Filters out external users from the pod-eligible list.
- src/app/(app)/settings/page.tsx — added Client Teams + Pods cards to
  the index, reordered to surface user-management first.

Verified: tsc --noEmit ✓ zero errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:25:29 -04:00
DJP
69f293682a Fix deploy port clash + wire admin invite → add-user flow
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>
2026-04-20 19:06:47 -04:00
DJP
eede696eee Phase 6a: local auth — credentials login + password reset flow
MVP auth is email + password (Entra SSO stays coded but env-gated via
NEXT_PUBLIC_AUTH_ENTRA_ENABLED for when the redirect URI is ready).
Uses a custom DB-session endpoint to mirror the existing MSAL pattern
at /api/auth/sso — no NextAuth strategy refactor needed.

API routes:
- POST /api/auth/login — email+password, bcrypt.compare, creates
  Auth.js-compatible DB session + Secure cookie. Constant-time
  behaviour (dummy-hash compare on missing user) to not leak account
  existence. Returns { ok, mustChangePassword } so the client can
  route first-login users to /change-password.
- POST /api/auth/forgot-password — issues a 1-hour single-use reset
  token. Never leaks enumerability (always 200). In dev, returns the
  reset URL in the response so admins can hand it over before SMTP
  is wired up. In prod, the token is only logged server-side.
- POST /api/auth/reset-password — validates token, bcrypt-hashes new
  password, clears token, flips mustChangePassword=false, and
  revokes all existing sessions so a stolen cookie can't linger.
- POST /api/auth/change-password — authenticated user changes their
  own password. Skips the current-password check for users without a
  passwordHash (covers first-time setup for SSO-seeded accounts).
  Clears mustChangePassword.

UI pages:
- (auth)/login — rewrote for email+password form. Entra SSO button
  only renders when NEXT_PUBLIC_AUTH_ENTRA_ENABLED=true. Dow brand
  block on the left ("Dow Jones Studio Tracker").
- (auth)/login/CredentialsLogin — client form, routes first-login
  users to /change-password?first=1.
- (auth)/change-password — forced password change after first login;
  also usable as a plain change-password screen.
- (auth)/forgot-password — email form → reset link. Shows dev link
  in-page when available.
- (auth)/reset-password/[token] — set new password from email link.

Middleware: /forgot-password and /reset-password added to the
authn-bypass allow-list alongside /login.

Minimum password length enforced at 10 chars. All API endpoints
return generic messaging to avoid information disclosure.

Verified: tsc --noEmit ✓ zero errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 19:00:15 -04:00
DJP
7598f4285e Phase 4+5: Dow XLSX ingest API + OMG webhook receiver
Shared canonical path: both ingest channels transform inputs into a single
DowRow shape (src/lib/validators/dow-import.ts) and write via the single
upsertProjectFromDow() function (src/lib/services/dow-excel-service.ts),
so the XLSX importer and the webhook cannot drift. Upserts on
Project.omgJobNumber (unique) — idempotent under replay.

XLSX ingest (Phase 4):
- New src/lib/validators/dow-import.ts — Zod schema with STATUS_MAP,
  RISK_MAP, header normalizer, team-slug normalizer, preview + commit
  result types.
- New src/lib/services/dow-excel-service.ts:
  - parseDowTracker(buffer): locates "Job Tracker " (or "Job Tracker"),
    scans first 5 rows to find the header row with 4+ matched columns,
    skips the example/instructions row at header+1, substring-matches
    headers (handles "Creative Team Member Deliverable is Assigned to"
    → assignee), collects row-level errors without aborting the batch.
  - upsertProjectFromDow(row, organizationId): auto-creates
    ClientTeam if missing (seed covers the 6 canonical teams, but stay
    forgiving); on create, generates N deliverables from outputCount +
    pipeline stages from the default Dow pipeline template with
    BLOCKED/NOT_STARTED status derived from stage dependencies; on
    update, only overwrites fields that are set so producer-edited data
    isn't clobbered by blanks.
  - previewDowImport() and commitDowImport() wrap the flow for the API.
- Rewrote src/app/api/projects/bulk-import/route.ts for the Dow schema.
  POST ?commit=true|false, multipart file=<xlsx>. commit=false returns
  {preview, totalRows, validRows, errors[], rows[]} (first 25 samples);
  commit=true returns {imported, created, updated, deliverablesCreated,
  errors[]}. Batch never aborts on a single bad row.

OMG webhook (Phase 5):
- New src/app/api/webhooks/omg/route.ts — POST-only. HMAC-SHA256
  signature verification via X-OMG-Signature: sha256=<hex> against
  OMG_WEBHOOK_SECRET, timing-safe compare. OMG_WEBHOOK_ALLOW_INSECURE
  escape hatch for stub testing. Looks up the Dow org by canonical
  domain dowjones.com. Transforms the (speculative, documented)
  OMG payload into DowRow then calls upsertProjectFromDow. Unknown
  fields from payload.raw land on Project.customFields JSON so OMG
  can add fields without us losing data. Logs every event (never
  the raw payload — PII).
- middleware.ts: /api/webhooks/ added to the unauthenticated-allowed
  path list (alongside /api/auth and /api/health) — HMAC auth happens
  inside the handler.

Verified: tsc --noEmit ✓ zero errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:57:06 -04:00
DJP
2c64356ffd Phase 3: Dow seed + rejection-routing automation
prisma/seed-dow.ts — idempotent seed for the Dow Jones tenant:
- Organization "Dow Jones" (dowjones.com)
- 6 ClientTeams: Brand, Events, B2B, Content, Briefing Team, Performance
- 3 placeholder Pods (Sergio, Deborah, Shared) — replace with real roster
  when it's available
- Dow Pipeline Template "Dow Jones Standard" with 11 stages:
  Pipeline → New → Copywriter → Client Review (Copy) → In Progress
  Creative → Internal Review → Client Feedback → Final Approval →
  Completed (+ On Hold, Canceled as terminal parking)
- Stage dependencies wired (optional stages bypass cleanly so
  In Progress Creative reaches from New when Copywriter is skipped)
- Automation rule "Client Feedback → reopen In Progress Creative":
  trigger on stage.status_changed where stageSlug=client-feedback and
  newStatus=CHANGES_REQUESTED. Actions: reopen sibling stage +
  increment revisionRound, send notification to assignee+producer.
- Initial admin user (DOW_ADMIN_EMAIL, default admin@dowjones.com)
  with bcrypt password and mustChangePassword=true. If
  DOW_ADMIN_PASSWORD env is unset a secure random is generated and
  logged once for handoff.
- RBAC defaults seeded per role including CLIENT_VIEWER.
- Legacy global PipelineStageTemplate rows seeded as FK scaffolding.

New action type "reopen_sibling_stage" in action-executor.ts:
- Given event.payload.deliverableId + params.siblingSlug, finds the
  sibling stage (matching either stageDefinition.slug or template.slug)
  and sets it to params.reopenStatus (default IN_PROGRESS). If
  params.incrementRound=true, bumps the stage's revisionRound counter
  and clears completedDate. Added to validateActions' allow-list.

Wiring:
- package.json db:seed → tsx prisma/seed-dow.ts (HP seed kept at
  db:seed-legacy for reference until deleted)
- prisma.config.ts migrations.seed → seed-dow.ts
- bcryptjs + @types/bcryptjs added

Verified: tsc --noEmit ✓ zero errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:53:20 -04:00
DJP
d953cee7ad Phase 2: per-client-team visibility enforcement
- New src/lib/rbac/visibility.ts: visibleProjectsWhere/Deliverables/Stages
  helpers + assertProjectVisible. ADMIN bypasses; empty team memberships
  fail-closed (user sees nothing). Reads from session cache, falls back
  to DB lookup.
- Session callback now populates clientTeamIds + isExternal on session.user
  so downstream queries don't hit the DB per request.
- next-auth.d.ts: Session.user extended with clientTeamIds + isExternal.
- AuthSession type mirrors the same.
- require-auth: added visibilityContextFromSession(session) helper so API
  routes can construct a VisibilityContext in one line.
- CLIENT_VIEWER role entry added to DEFAULT_PERMISSIONS (read + comments).

Services wired with visibility (32 query sites across 9 files):
- project-service: list/get AND'd with visibleProjectsWhere; update/delete
  pre-gate via assertProjectVisible.
- deliverable-service: list/get/create/bulkCreate gate on parent project
  visibility; update/delete pre-check via parent project lookup.
- stage-service: getBlockedStages AND's stage visibility;
  bulk/updateStageStatus pre-gate via parent project.
- dashboard-service: all 6 groupBy/findMany queries AND'd with visibility.
- workload-service: pulls project.clientTeamId and post-filters assignments
  (nested include can't be filtered cleanly at DB level).
- calendar-service: now takes organizationId + ctx; AND's org + visibility
  into the stage findMany.
- weekly-report-service: 6 parallel queries AND'd with visibility fragments.
- semantic-search-service: Prisma queries AND'd; raw SQL vectorSearch
  appends `AND p."clientTeamId" = ANY($N::text[])` for non-admins, returns
  empty early when scoped user has no team memberships.
- assignment-service: assignUserToStage pre-gates project visibility;
  getMyWork filters rows by client-team membership; bulkAssignArtists
  skips stages not visible to caller.

API routes updated to pass visibility context (13 routes):
/api/projects, /api/projects/[id], /api/projects/[id]/deliverables,
/api/projects/[id]/deliverables/[id], /api/stages/[id],
/api/stages/[id]/assignments, /api/dashboard/stats, /api/my-work,
/api/calendar, /api/reports/weekly, /api/workload,
/api/search/semantic, /api/chat/route (chat tool-executor threads ctx
through all 20 tool handlers via executeTool context param).

Verified: npx tsc --noEmit ✓ zero errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:50:00 -04:00
DJP
cadffa4bd6 Phase 1: Dow-customized Prisma schema + strip HP-only features
Schema changes:
- Add ClientTeam, ClientTeamMembership (visibility grouping — Brand/Events/etc)
- Add Pod, Pod.leadUser/members (capacity grouping orthogonal to ClientTeam)
- Add local-auth fields on User (passwordHash, reset tokens, mustChangePassword,
  lastLoginAt, isExternal) — coexists with Entra SSO, flipped on post-MVP
- Add CLIENT_VIEWER role (read-only external Dow Jones users)
- Add ProjectStatus.PIPELINE (unaccepted brief) and ProjectStatus.CANCELED
- Add Permission.CLIENT_TEAM_MANAGE and POD_MANAGE
- Project: add clientTeamId (visibility FK) and omgJobNumber (canonical ingest key)

Removals (HP-specific approval workflow, not needed for Dow):
- Model ColorProbe (HP-CMF eyedropper)
- Model ReviewSession + ReviewSessionItem (batch approval)
- Model FeedbackItem + enum FeedbackStatus (formal OPEN→RESOLVED→VERIFIED chain)
- All cross-relations on User, Revision, Comment, Annotation, DeliverableStage

Migration: squashed HP baseline into clean 20260420000000_init with
CREATE EXTENSION IF NOT EXISTS vector; at top for non-docker deployments.

Code plumbing:
- DEFAULT_PERMISSIONS: added CLIENT_VIEWER entry (read + COMMENT_CREATE only)
- org-scope.ts: added clientTeam + pod cases, removed colorProbe/feedbackItem/reviewSession
- Deleted 29 files: review pages, review API routes, feedback/color-probe
  components + services + validators + hooks
- Stripped eyedropper tool from annotation-tools.tsx, use-annotation-state.ts,
  video-annotation-layer.tsx
- Removed "Reviews" nav entry from sidebar
- Deleted src/lib/utils/color.ts (CMF-only, unused after ColorProbe removal)

Verified: prisma validate ✓, npx tsc --noEmit ✓ (zero errors)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:35:14 -04:00
DJP
60ace61fd9 Merge branch 'main' of bitbucket.org:zlalani/dow-prod-tracker 2026-04-20 18:22:08 -04:00
DJP
51e0cf44c7 Phase 0: fork rebrand from hp-prod-tracker
- 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>
2026-04-20 18:21:39 -04:00
Dave Porter
2826f55479 Initial commit 2026-04-20 20:58:59 +00:00
Vadym Samoilenko
de4c862372 Fix signOut redirect: include basePath /hp-prod-tracker in redirectTo 2026-04-16 19:14:12 +01:00
Vadym Samoilenko
80114a65c8 Add sign-out button to sidebar
Exits the app session only (no Microsoft global logout).
Auth.js signOut() deletes the DB session and clears the cookie,
then redirects to /login.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 19:06:10 +01:00
Vadym Samoilenko
250796dd0c Replace Auth.js OAuth with MSAL.js SPA browser flow
- 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>
2026-04-16 18:49:43 +01:00
Vadym Samoilenko
6701946092 Fix OAuthRelay: relay on code-only, drop state check
Azure SPA returns ?code&session_state (no OAuth state). Auth.js also omits
state from the authorization URL when using PKCE. Two fixes:
- OAuthRelay: trigger on `code` alone, forward all params as-is
- auth.ts: checks: ["pkce"] — removes state requirement Auth.js would fail on

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 15:42:06 +01:00
Vadym Samoilenko
cadba79f55 deploy.sh: replace SECRET check with AZURE_REDIRECT_URI
SPA registration has no client_secret; check the new required var instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 15:30:52 +01:00
Vadym Samoilenko
17fc539d19 Configure SSO for Azure SPA registration: PKCE without client_secret
- Override authorization redirect_uri to match Azure SPA portal registration
  (login page URL instead of Auth.js callback URL)
- Custom token.request: public client PKCE exchange — no client_secret sent
- Add OAuthRelay client component: forwards ?code&state from login page to
  /api/auth/callback/microsoft-entra-id via window.location.replace
- Add AZURE_REDIRECT_URI env var to docker-compose.yml and .env.example
- Remove AUTH_MICROSOFT_ENTRA_ID_SECRET (SPA registrations don't issue secrets)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 15:25:57 +01:00
Vadym Samoilenko
bf0bee9c28 Fix SSO: use /api/auth (no basePath) as OAuth redirect_uri
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>
2026-04-15 15:54:37 +01:00
Vadym Samoilenko
f5b091ceea Fix auth error redirect: include basePath in signIn page path
Auth.js constructs server-side redirects from origin only, ignoring the
Next.js basePath. Explicitly including /hp-prod-tracker in pages.signIn
ensures errors redirect to /hp-prod-tracker/login instead of /login.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 15:33:47 +01:00
Vadym Samoilenko
c412ad7bdf Fix health check URL to include basePath
App is served under /hp-prod-tracker basePath, so the health endpoint
is at /hp-prod-tracker/api/health not /api/health.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 15:29:43 +01:00
Vadym Samoilenko
1b07542a31 Fix SSO token exchange: restore redirectProxyUrl alongside explicit redirect_uri
authorization.params.redirect_uri fixes the authorization request URI.
redirectProxyUrl fixes the token exchange URI (beta.30 uses it there).
Both are needed. AUTH_URL must now include /api/auth suffix on the server.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 15:22:01 +01:00
Vadym Samoilenko
1950ecc7d6 Restore Apache step: add Include to /etc/apache2 on deploy
On first deploy replaces the old inline hp-prod-tracker block in
optical-dev.oliver.solutions.conf with an Include pointing to
apache/hp-prod-tracker.conf. Idempotent — skips if Include already present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 15:16:30 +01:00
Vadym Samoilenko
b7d50ad715 Move Apache config to apache/hp-prod-tracker.conf, remove auto-management
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>
2026-04-15 15:15:44 +01:00
Vadym Samoilenko
63818bc6e2 Rewrite deploy.sh following ppt-tool pattern
Numbered steps matching server conventions: prerequisites install,
git pull with SSH auto-switch, .env validation, docker compose build,
postgres + health-check waits, idempotent Apache Include management,
UFW firewall. Apache step replaces old inline block with a canonical
Include pointing to deploy/apache.conf.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 15:13:24 +01:00
Vadym Samoilenko
548a9d8ef5 Add Apache config snippet and wire into deploy script
deploy/apache.conf: canonical Apache proxy config for hp-prod-tracker —
adds WebSocket passthrough and 500 MB upload limit missing from the
current inline config. deploy.sh now replaces the inline block with an
Include directive on each deploy so the config stays in source control.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 15:11:00 +01:00
Vadym Samoilenko
7e32bbc430 Add idempotent deploy script
Handles initial deploy and updates: git pull via SSH, docker compose
rebuild, health check with timeout, pre-flight .env validation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 15:08:29 +01:00
Vadym Samoilenko
6fd240860c Fix SSO redirect URI by setting authorization.params explicitly
next-auth v5 beta ignores redirectProxyUrl when constructing the
redirect_uri sent to Microsoft — it strips the pathname from AUTH_URL
and uses only the origin. Passing redirect_uri directly in
authorization.params guarantees the /hp-prod-tracker basePath is
included in the callback URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 15:04:42 +01:00
DJP
aae25a0959 Fix SSO redirect URI to include basePath via redirectProxyUrl
Auth.js route matching needs basePath="/api/auth" (Next.js strips
/hp-prod-tracker from the internal request). But the OAuth redirect_uri
sent to Microsoft must include the full external path.

Uses redirectProxyUrl to explicitly set the callback URL to
{AUTH_URL}/api/auth/callback/microsoft-entra-id, which includes
the /hp-prod-tracker basePath. Pins basePath="/api/auth" so
AUTH_URL's pathname doesn't override route matching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 13:00:01 -04:00
DJP
f41dfe6024 Pass AUTH_URL through to container for SSO callback
Auth.js needs AUTH_URL to build the correct redirect URI
including the /hp-prod-tracker basePath.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 15:39:09 -04:00