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>
16 KiB
Dow Jones Studio Tracker — How-to Guide
Everything you need to get, run, use, admin, integrate, and extend the tracker. Pairs with DEPLOY.md (prod deploy) and API.md (REST/webhook reference).
Table of contents
- Quick mental model
- Run it locally
- Run it in production
- The first-login ritual
- Add users
- Configure client teams + pods
- Get real data in (XLSX)
- Wire the OMG webhook
- Day-to-day: producer workflow
- Day-to-day: external client viewer
- Resource planning
- Who can do what (RBAC)
- Common problems + fixes
- Change the model
Quick mental model
One tracker, one tenant. Every row is a Project — equivalent to one row in
Dow's existing Studio Tracker XLSX. Projects belong to a ClientTeam (Brand,
Events, B2B, Content, Briefing Team, Performance) which drives who can see
what. Under each project are deliverables, and each deliverable runs through
the Dow 11-stage pipeline:
Pipeline → New → Copywriter → Client Review (Copy) → In Progress Creative
→ Internal Review → Client Feedback → Final Approval → Completed
± On Hold / Canceled (terminal parking states)
Orthogonal to visibility, everyone belongs to a Pod (Sergio's / Deborah's / Shared) — pods are for capacity planning, not for project access.
The omgJobNumber is the canonical key. Both the XLSX importer and the OMG
webhook upsert on it, so if OMG publishes a job update while someone's editing
the same row in the UI, the next ingest correctly merges onto that row.
Run it locally
Prereqs: Docker Desktop, Node 20+, SSH key on Bitbucket.
# 1. Clone
cd ~/Desktop/CODING-2024
git clone git@bitbucket.org:zlalani/dow-prod-tracker.git DOW-PROD-TRACKER/dow-prod-tracker
cd DOW-PROD-TRACKER/dow-prod-tracker
npm install
# 2. Env
cp .env.example .env
# Fill in AUTH_SECRET at minimum:
sed -i '' "s|^AUTH_SECRET=.*|AUTH_SECRET=\"$(openssl rand -base64 32)\"|" .env
# Set NEXT_PUBLIC_AUTH_ENTRA_ENABLED=false (default) so local auth works.
# 3. Start the DB (compose handles port clashes via the ${DB_HOST_PORT:-5492} default)
docker compose -p dow-prod-tracker up -d db
# If 5492 is busy on your Mac (e.g. another project's postgres), create a local
# override — gitignored:
cat > docker-compose.override.yml <<EOF
services:
db:
ports: !override ["5493:5432"]
EOF
# And update DATABASE_URL in .env to use :5493 instead of :5492.
# 4. Migrate + seed
npx prisma migrate deploy
npm run db:seed # prints admin email + temp password — SAVE THEM
# 5. Dev server
npm run dev
# → http://localhost:3000/dow-prod-tracker
# (or :3001 if Docker Desktop occupies :3000 on your Mac)
Sign in with the seed admin → forced password change → Dashboard.
Run it in production
See DEPLOY.md. One-liner summary:
cd /opt/dow-prod-tracker
git pull
./deploy.sh
docker compose -p dow-prod-tracker exec app npm run db:seed # first deploy only
deploy.sh auto-picks free host ports (3002 / 5492 preferred), renders the
Apache snippet, reloads the vhost, configures ufw. Idempotent.
The first-login ritual
When the seed runs, it prints:
Email: admin@dowjones.com
Password: <16-char random>
Sign in with those. The app forces a password change on first login:
- Go to
/login→ enter email + temp password - Land on
/change-password?first=1→ enter temp password + new password × 2 - On success → redirected to the Dashboard
The mustChangePassword flag is DB-tracked per user. Invited users go through
the same flow via /reset-password/<token>.
Add users
The "add user" flow is an invitation that creates both the User row AND a password-reset token in one go. No separate "accept" step — the invitee just sets their password via the reset link.
Through the UI
- Sign in as admin
- Settings → Team
- Type email, pick a role (Admin / Producer / Artist / Client (read-only)), click Send Invite
- A green banner appears with the accept URL. Click Copy, paste into Teams / email / Slack to hand it to the user
- User visits the URL → sets password → signs in
Programmatically
curl -X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-d '{"email":"newuser@oliver.agency","role":"PRODUCER"}' \
https://your-host/dow-prod-tracker/api/org/invitations
# → response includes { "acceptUrl": ".../reset-password/<token>" }
Notes
- Role choice matters for what they see:
ADMIN— everything across all client teams.PRODUCER— the studio side. Can create/update projects and bookings.ARTIST— can update stage statuses / submit revisions but not create projects.CLIENT_VIEWER— external user. Read-only. Only sees the client teams they're explicitly assigned to.
- Invitation tokens expire in 7 days. Just re-invite to re-issue.
- Placeholder seed users exist but can't log in until an admin invites them (they have no passwordHash). Go to Settings → Team and invite any of them — their role/pod/department are already set.
Configure client teams + pods
Client teams
Six teams are seeded: Brand, Events, B2B, Content, Briefing Team, Performance.
- Settings → Client Teams
- Click a team to see its members
- Add users via the dropdown → they'll now see projects on this team
- Admins see all teams by default regardless of membership
A user who belongs to zero client teams sees zero projects (fail-closed).
Pods
- Settings → Pods
- Three placeholder pods seeded: Sergio's / Deborah's / Shared
- Create new pods or rename existing ones
- Each user has one
homePod. The Resources page groups bydepartmentfor now — swap to pod-grouping when the roster stabilizes
Get real data in (XLSX)
The fastest way to populate the tracker from Dow's existing spreadsheet.
UI path
- Projects → Import XLSX
- Pick the file (expected: a Dow Studio Tracker workbook with a
Job Trackersheet — trailing space tolerated, example/instructions row 2 auto-skipped) - Preview opens with normalized rows + per-row errors
- Review errors. Hit Commit when you're happy
- 18-ish projects land, ~250 deliverables are auto-created, pipeline stages attach
API path
# dry-run
curl -X POST \
-H "X-API-Key: $API_KEY" \
-F "file=@Dow_Studio_Tracker_2026_04_20.xlsx" \
"https://your-host/dow-prod-tracker/api/projects/bulk-import?commit=false"
# commit
curl -X POST \
-H "X-API-Key: $API_KEY" \
-F "file=@Dow_Studio_Tracker_2026_04_20.xlsx" \
"https://your-host/dow-prod-tracker/api/projects/bulk-import?commit=true"
Idempotency
Re-upload tomorrow's snapshot of the same tracker:
- Projects with an existing
omgJobNumber→ fields merge into the existing row (blanks don't clobber producer-edited data) - Projects with a new
omgJobNumber→ freshly created + pipeline stages spawned - Dropped rows in the XLSX are NOT deleted (would be destructive). To kill a
project, do it in the UI or via
DELETE /api/projects/:id.
See API.md § XLSX upload for the full column map and ingest edge cases.
Wire the OMG webhook
The long-term ingest channel. Set it up once, OMG pushes updates in real time.
1. Pick a secret
openssl rand -hex 32
2. Set it on the server
# /opt/dow-prod-tracker/.env
OMG_WEBHOOK_SECRET="<the hex string>"
OMG_WEBHOOK_ALLOW_INSECURE="false" # ensure this is false in prod
Restart the app container to pick up the env:
docker compose -p dow-prod-tracker up -d --force-recreate app
3. Give the secret + endpoint to Shashank / OMG
- URL:
https://optical-dev.oliver.solutions/dow-prod-tracker/api/webhooks/omg - Method:
POST - Content-Type:
application/json - Signature header:
X-OMG-Signature: sha256=<hex HMAC-SHA256 of the raw body using the shared secret> - Payload: see API.md § OMG webhook
4. Test with a stub payload
SECRET="$(grep OMG_WEBHOOK_SECRET /opt/dow-prod-tracker/.env | cut -d= -f2- | tr -d '"')"
BODY='{"event":"job.updated","timestamp":"2026-04-21T12:00:00Z","job":{"number":"TEST-001","name":"Stub job","team":"Brand","status":"In production"}}'
SIG=$(printf "%s" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -i -X POST \
-H "Content-Type: application/json" \
-H "X-OMG-Signature: sha256=$SIG" \
-d "$BODY" \
https://optical-dev.oliver.solutions/dow-prod-tracker/api/webhooks/omg
Expected: 200 { "ok": true, "projectId": "..." }. A TEST-001 project should
now be visible on the Brand team.
5. Replay / idempotency
Same (number, timestamp) replayed = noop. OMG can safely replay queues.
6. Verification
Both paths write through the same upsert function (upsertProjectFromDow
in src/lib/services/dow-excel-service.ts). If XLSX works but the webhook
doesn't, it's the signature or the HTTP layer — not the data.
Day-to-day: producer workflow
The happy path.
- Dashboard — glance at the KPIs, overdue deliverables, recent activity
- Projects — the Excel-shaped grid. Filter by team, search by OMG #, sort by deadline. Click a project to open it
- Project detail — see deliverables + their pipeline stages. Advance stages as work progresses (click the stage status badge → pick the new status). Transitions are gated: you can't jump to In Progress Creative before New is accepted
- Upload XLSX / sync from OMG — both are additive. Producer-edited data survives re-ingest
- Client Feedback → CHANGES_REQUESTED — automation rule kicks in, re-opens In Progress Creative on the same deliverable and increments its revision round. Notifies the assignee + PM
- Resources page (admins + producers) — assign N hours of a job to a person on a day. Capacity bars go amber at 85%, red over 100%
Day-to-day: external client viewer
For @dowjones.com users you want to give read-only visibility.
- Admin invites them with role
CLIENT_VIEWER(Settings → Team → Client (read-only)) - Admin adds them to exactly the client teams they should see (Settings → Client Teams → Add member). Usually one team; could be multiple
- User sets password via the reset link
- On sign-in they see:
- Only projects on teams they're a member of
- Dashboard KPIs scoped to those same projects
- No edit buttons (the UI checks
session.user.role === "CLIENT_VIEWER"and hides mutations) - Can still comment (that's intentional — client feedback is normal for review flows and doesn't mutate state)
- Any attempt to POST/PATCH/DELETE anything returns 403.
When you're ready for Entra guest-invite SSO instead of local accounts, set
NEXT_PUBLIC_AUTH_ENTRA_ENABLED=true and fill in the Azure env vars. No
code change.
Resource planning
The Resources page (in the sidebar) is the daily-hours capacity view.
- Users grouped by department (swap to pod grouping when the real roster lands)
- Each cell = one person × one weekday. Shows job chips + capacity bar
- Click Assign on any cell → job-number autocomplete (pulls from your
project
omgJobNumberlist) + hour picker → commit - Job chips are color-coded by a hash of the job number — same number always gets the same color across the grid
- Over-cap days get a red outline + a "8h/6h" red badge
ADMIN + PRODUCER can write; ARTIST / CLIENT_VIEWER see read-only.
Who can do what (RBAC)
Defaults per role (Settings → Permissions to view / customize per org):
| Role | Key permissions |
|---|---|
ADMIN |
Everything. Managing users, teams, pods, pipelines, automations. |
PRODUCER |
Projects CRUD, deliverables, stages, revisions, comments, bookings. Can't manage users/teams/pods. |
ARTIST |
Read projects/deliverables/stages. Update stage status + submit revisions. Can comment. |
CLIENT_VIEWER |
Read-only on visible projects. Comments only (no state mutation). |
Per-team visibility is layered on top — an Artist on Team X only sees Team X's projects, an Admin sees everything.
Common problems + fixes
"No organizations found" on /api/health
You haven't run the seed. docker compose -p dow-prod-tracker exec app npm run db:seed.
Seed says tsx: not found
The runner image is missing the dev deps. Fixed in commit df7ddbf (installs
tsx globally). Rebuild with --no-cache:
docker compose -p dow-prod-tracker down
docker compose -p dow-prod-tracker build --no-cache app
docker compose -p dow-prod-tracker up -d
Browser returns 404 at /dow-prod-tracker/...
Apache Include didn't land. Check:
grep dow-prod-tracker /etc/apache2/sites-enabled/optical-dev.oliver.solutions.conf
If absent, re-run deploy.sh (it auto-detects whether sites-enabled is a
symlink to sites-available or a separate file).
Port 5492 is busy on the server
deploy.sh auto-picks the next free port. If you want to force a specific
one, DB_HOST_PORT=5499 ./deploy.sh.
Password reset email isn't sent
SMTP isn't wired yet. POST /api/local-auth/forgot-password returns the
reset URL in the response when NODE_ENV !== production (dev path). In
prod, the URL is only server-logged. For now: admin reads the URL from the
create-invite response and hands it to the user out-of-band.
"Missing permission: X" on an action
Check the role. Settings → Team → see everyone's role. Only ADMIN gets full powers by default.
Apache 502 after deploy
App container didn't start. docker compose -p dow-prod-tracker logs app --tail 50.
XLSX upload rejects most rows
Usually means the Job Tracker sheet's headers drifted or Excel wrapped some
cells as hyperlinks. The importer handles the common cases; paste the
?commit=false preview errors and we'll iterate.
OMG webhook returns 401
Shared secret mismatch. Double-check both sides have the same
OMG_WEBHOOK_SECRET and that the signing header is
X-OMG-Signature: sha256=<hex> (lowercase sha256=, no extra whitespace).
Change the model
When the shape of "a job" changes, edits go here:
| Field of interest | Where to touch |
|---|---|
| Add a Project field | prisma/schema.prisma → new migration → src/lib/validators/project.ts → src/components/projects/project-form-dialog.tsx → optionally the Projects-page table column |
| Add/change an enum value | schema.prisma enum → new migration (Postgres: ALTER TYPE ADD VALUE) → update map/usage |
| Rename a ClientTeam | UI only — slugs are immutable (ingest depends on them) |
| Add a new pipeline stage | prisma/seed-dow.ts → DOW_STAGES + DOW_DEPENDENCIES → re-run seed |
| New XLSX column to ingest | src/lib/validators/dow-import.ts (schema) + src/lib/services/dow-excel-service.ts (HEADER_MATCHERS + upsertProjectFromDow) |
| New OMG webhook event | src/app/api/webhooks/omg/route.ts — the switch statement on event |
| New automation action | src/lib/automation/action-executor.ts — add a case + register it in validateActions |
Migrations: the repo is on Prisma 7 with a clean baseline plus one delta. For a new schema change, generate a fresh migration against a running local DB:
npx prisma migrate dev --name <short_description>
If a local DB isn't running, hand-write the SQL into
prisma/migrations/<timestamp>_<name>/migration.sql (idempotent, forward-only).