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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
- 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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Use request.nextUrl.clone() instead of new URL("/login", request.url)
so Next.js includes the /hp-prod-tracker basePath in redirects.
Without this, unauthenticated users get sent to /login instead of
/hp-prod-tracker/login.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
README.md:
- Full project overview, tech stack, features, AI architecture
- Deployment guide, data model, RBAC matrix, project structure
provider.ts:
- Reduce Ollama timeout from 180s to 45s (fail fast to Claude)
- Smart escalation: when Ollama responds with 0 tool calls but the
query likely needed data (keyword match), automatically escalate
to Claude for reliable tool calling
- Ollama still handles pure conversational queries for free
- Queries needing real data get Claude's reliable tool calling
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of sending all 12 tools every request, match the user's message
against keyword groups (status, workload, assign, create, advance, revision)
and only send relevant tools. search_entities always included for name
resolution. Falls back to basic query tools if no keywords match.
This cuts the tool definitions from ~12 to ~2-6 per request, significantly
reducing context size for gemma4.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Filter tools to 12 (from 17) via OLLAMA_TOOL_ALLOWLIST
- Shorten tool descriptions to first sentence only
- Trim system prompt: drop pipeline details and suggestion format, keep Rules
- Reduce num_predict from 4096 to 2048
- Fix system prompt trimming to preserve Rules section (name resolution, mutation flow)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ollama's parser chokes on deeply nested JSON in tool_use/tool_result
structured content blocks. Instead of sending OpenAI-format tool
messages, flatten everything to simple role/content text messages.
Tool results are truncated to 2KB to keep context manageable.
The model still receives tool definitions and can make new tool calls,
but prior tool interactions are shown as plain text in the history.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ollama was receiving chunked transfer encoding from Node.js fetch and
failing to parse the JSON body ("can't find closing '}' symbol").
Sending a Buffer with explicit Content-Length forces a single complete
body write.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logs request size, message count, and detailed error info to help
diagnose the "can't find closing '}'" JSON parsing error from Ollama.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gemma 4 loads successfully, supports tool calling with proper
structured output, and responds in ~100ms after initial load.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Larger models (mistral-large 122B, qwen3-coder 30B, gpt-oss 20B) all
fail to load due to resource limits. mistral:latest (7.2B) loads and
responds successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mistral-large:latest requires 420GB RAM, server only has 345GB.
qwen3-coder:30b is a 30.5B MoE model that fits in ~20GB with good
tool calling and reasoning capabilities.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Ollama (internal GPU server) is tried first — free
- If Ollama is down, falls back to Claude API with a browser toast:
"Ollama unavailable — using Claude (paid API)"
- Provider badge shows which one is active (orange/purple)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Claude is primary, Ollama (internal GPU server) is automatic fallback
- Provider auto-selects: Claude if API key set, else Ollama if reachable
- Ollama uses mistral-large:latest for chat with full tool calling support
- Removed local Ollama Docker service — uses remote at 10.24.42.219
- Chat panel badge shows "Claude" (purple) or "Ollama" (orange)
- OLLAMA_CHAT_HOST and OLLAMA_CHAT_MODEL env vars for configuration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Cap conversation history to last 20 messages
- Truncate tool results over 8KB before sending back to Claude
- Trim long assistant messages in client-side history to 2KB
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The env var was in .env but not listed in docker-compose environment
block, so the container never received it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>