Commit graph

40 commits

Author SHA1 Message Date
Vadym Samoilenko
573760f34e feat: add monique.mcconnell@brandtech.plus to allowlist 2026-05-15 13:38:06 +01:00
Vadym Samoilenko
d9b5f2df83 feat: add ameliagreene@oliver.agency to allowlist 2026-05-11 13:41:58 +01:00
DJP
06110d6d71 fix: NV review round 1 — pipeline blockers + UX feedback + hidden bug hardening
Phase 1 (pipeline blockers):
- Stage 6 normalizer: move Claude call to BackgroundTasks (fixes 504 at proxy);
  frontend polls useStageArtifacts(6) + useClientAssets every 2s with a
  2-min soft cap and a "Normalizing… ~30s" banner.
- Stage 8 ratecard: raise ValueError when no Stage-7 matches selected so
  the user gets a clear 400 instead of silent "0 lines built" success.
- Stage 4 Q&A pack: visible amber empty-state callout when no clarifications.
- Stage 1 intake: green CTA banner after metadata lands telling the user
  to scroll down and click Complete Stage 1.
- APP_PUBLIC_URL: log a warning at startup if empty / not fully-qualified
  so approval-email links don't ship as broken relative URLs.

Phase 2 (reviewer UX):
- Remove "Operating model" select from intake form (sales lead doesn't
  know the solution yet); default model_type='current_oplus'.
- Inline-edit pencil for opportunity name in OpportunityView header.
- Stage 3 TROWLS sliders default to 0/10 (was 5/10 — anchored everyone
  to "average" and the reviewer could save without engaging).
- Trim APPROVAL_ROLES to ['commercial', 'solution'] (was 5 roles).
- Stage 6 confirm dialog only fires on re-run, not first run.
- "+ Add manually" → "+ Add deliverable manually" with helper text.
- Asset normalizer prompt + post-hoc stop-list filter excluding internal
  pitch artefacts (pitch decks, response decks, win-themes, etc.) that
  were appearing as job routes in Stage 7.

Phase 3 (hardening):
- with_for_update() row locks on stage_machine.complete_stage and
  approvals.submit_decision so double-clicks can't double-advance.
- 30s idempotency window on Stage 7 matching kick-off.

Deferred (next round): paste-link upload, single-use approval tokens,
FK indexes migration, datetime.utcnow → now(timezone.utc) sweep,
notes-owner schema change, file-extraction "unsearchable" UI badge.

Source: REVIEW-SESSIONS/Sales Op Platform Feedback NV_ 060526.xlsx
(rows R6-R48, 25+ feedback items mapped to specific stages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:30:48 -04:00
DJP
9b00028ca9 Fix complete-stage button + delete opportunity (UI + on-disk cleanup)
- OpportunityView: Complete-Stage button was gated on the URL :stageNumber
  param and hid itself on bare /opportunities/:id URLs (e.g. when navigating
  from the Dashboard). Now keyed off activeStage / stageState only.
- Dashboard: add per-card delete button with confirm dialog.
- Backend DELETE /opportunities/{id}: remove on-disk uploads dir as well as
  the cascade-deleted DB rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:12:17 -04:00
Vadym Samoilenko
df4cab74b8 feat: add nicky.vissers and rudi.reagon to allowlist 2026-05-05 13:02:29 +01:00
Vadym Samoilenko
31b273785a fix: user menu UX — stable dropdown, email ellipsis, auto-redirect on sign-out
- Replace onMouseLeave with click-outside handler so dropdown stays
  open while moving mouse inside it
- Email truncated with ellipsis instead of mid-character wrap
- clearCache() followed by reload() so login screen appears immediately
  without manual refresh

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 11:09:18 +01:00
Vadym Samoilenko
ac659ea546 fix: sign-out clears local session only, not Microsoft account
Replaces logoutRedirect with clearCache() so the user returns to
the app login screen without being signed out of their Microsoft
account.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 10:59:51 +01:00
Vadym Samoilenko
7b6b09b607 feat: add user avatar + sign-out button to nav
Shows initials avatar in top-right nav; click opens a dropdown with
name, email and Sign out button (logoutRedirect). Hidden in dev-bypass
mode. Fixes the missing logout affordance for SSO users.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 10:56:41 +01:00
Vadym Samoilenko
ee0ca9ccd1 feat: populate SSO allowlist with approved users
Adds 7 users (6 editors + 1 admin) now that Azure AD SPA redirect URI
is confirmed by IT. Removes placeholder zlalani entry which has no
corresponding Azure account.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 10:43:07 +01:00
Vadym Samoilenko
c7025ee396 feat: enable SSO with email allowlist authorization
- Add config/allowed_users.yaml as the source of truth for access control
  (email → role mapping, case-insensitive)
- New backend/app/services/allowlist.py loads the YAML and provides lookup()
- auth.py checks the allowlist on every SSO login; denies with 403 if not listed;
  syncs AppUser.role from YAML on each login
- PyYAML added to requirements.txt
- docker-compose mounts ./config:/app/config into the backend container
- Frontend: axios response interceptor catches 403 not_allowlisted and fires
  a custom DOM event; AuthProvider renders a NoAccessPage with Sign out button
- .env.example: clarify DEV_AUTH_BYPASS usage, document ALLOWED_USERS_PATH

Azure AD: add https://optical-dev.oliver.solutions/oliver-sales-ops-platform/
as a SPA redirect URI in app registration 9079054c (done separately by zlalani).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 19:27:39 +01:00
DJP
bcc3200ebb About page: confidence-building How-it-works
Rewrote /about so first-time users land somewhere that explains the
pipeline honestly and gives them reasons to trust the output.

Layout (top → bottom):
1. Header — one line of plain English about what the platform does +
   the "every AI run is grounded in your documents" claim.
2. Trust pillars row (4 cards):
   - Audit trail per stage (cost + tokens stored)
   - Two hard gates (named-role sign-off)
   - Tested invariants (118 tests, deterministic maths)
   - Cost is visible (~$1.50/run, per-panel pill)
3. The pipeline — the existing Mermaid flowchart with framing copy.
4. What each stage does — every stage as an expandable row with:
   - Driver badge (AI AGENT / HUMAN / EXPORT / APPROVAL GATE /
     DETERMINISTIC / PHASE 2)
   - 1-sentence description
   - On expand: endpoint · driver · inputs · output · "what the
     agent is told" (the actual prompt rules in plain English) ·
     cascade behaviour
5. What we do — and don't do — with AI: two columns spelling out
   the trust contract (ground in uploaded docs, structured tool-use,
   never hide costs · don't auto-complete, don't AI the maths,
   don't pretend the AI is right).
6. If something looks wrong — the recovery instincts: every agent
   has a Re-run, normalize is destructive by design (with warning),
   ratecard maths is unit-tested, approval emails carry tokens, the
   README is the source of truth.

Production build clean (tsc + vite build, 0 errors).

The agent prompts shown on this page are paraphrased from the actual
system prompts in backend/app/services/*_agent.py — same content,
human-friendly framing. The README has the verbatim prompts.
2026-04-27 19:31:05 -04:00
DJP
7254d95f32 Add README with per-stage agent reference
A single document a human can read to understand the whole pipeline:

- High-level: what the platform is + a 17-row stage table.
- Quick start (local dev) + smoke test commands.
- Deployment to optical-dev: deploy.sh flow, port-pick logic,
  Apache include line, what to do on first boot vs re-run.
- Architecture summary (state machine, artifacts, approvals, cost).
- Per-stage agent reference (the heart of the doc): for each of the
  9 Claude agents — what it's for, what it reads, output schema, and
  the system-prompt rules in plain English. Plus the non-Claude
  stages (qualification scorecard, Q&A export, ratecard build,
  efficiency profile, team shape) explained the same way.
- Cross-cutting concerns: cost tracking, approvals + Mailgun, auth
  paths (dev-bypass vs Azure JWT), exact stage-machine rules,
  destructive-cascade rules per stage.
- Testing: how to run the 118-test suite + what each file covers.
- Repo layout.

Goal: someone landing on the repo can read this front-to-back in 10
minutes and know what every stage does, what each agent is told, and
how to run / deploy / test the thing.
2026-04-27 18:31:21 -04:00
DJP
b553bef43a deploy: pipe DEV_AUTH_BYPASS into the Vite build
Vite inlines import.meta.env.VITE_DEV_AUTH_BYPASS at build time. The
build container wasn't getting it, so even with DEV_AUTH_BYPASS=true on
the backend the SPA still rendered the MSAL login gate.

Resolve VITE_DEV_AUTH_BYPASS, falling back to DEV_AUTH_BYPASS if it's
not explicitly set, and pass it through `docker run -e` to the build.
A single DEV_AUTH_BYPASS=true in .env now controls both halves.
2026-04-27 17:12:46 -04:00
DJP
ed0746267b deploy: auto-pick free ports + render Apache conf from template
The script bailed when 8003 was taken on the dev server. Per spec, it
should never block on a port clash — find a free port and run with it.

How it picks ports:
- Reads OSOP_DB_PORT / OSOP_REDIS_PORT / OSOP_BACKEND_PORT from .env,
  falling back to defaults 5435 / 6380 / 8003.
- For each, if the preferred port is taken on the host, scans upward in
  a sane range (5435-5499 / 6380-6399 / 8003-8099) for the next free one.
- Persists chosen ports back to .env via an idempotent KEY=VALUE upsert,
  so subsequent deploys keep using the same allocation.
- If our compose project is already running, skips the scan and reuses
  the current ports (re-deploy in place).

Compose port mappings now reference those env vars with defaults:
  127.0.0.1:${OSOP_DB_PORT:-5435}:5432, etc.

Apache config templating:
- deploy/apache-osop.conf.tmpl has __BACKEND_PORT__ placeholder.
- The script renders it to deploy/apache-osop.conf each run with the
  chosen backend port substituted in. The rendered file is gitignored
  (the template is the source of truth in git).
- If the backend port changed (or the Apache vhost doesn't yet Include
  our conf), the script tells the user to reload Apache.

This means a fresh server hits the conflict on 8003 (something else is
listening), the script picks 8004 silently, writes it to .env, renders
apache-osop.conf with 8004, brings the stack up, and tells you to
reload Apache. Re-running the script on the same server keeps 8004.
2026-04-27 16:34:05 -04:00
DJP
0734afedac deploy: fix silent exit when ports are free (lsof + pipefail)
The port-conflict check called lsof inside a pipeline. When no process
is listening (the success path on a fresh server) lsof returns 1, and
under `set -euo pipefail` that killed the script silently right after
the "Checking host ports" line.

Wrap the lsof/ss invocations in `{ … || true; }` and the call site
with `|| true`. Switched the function to `printf` so we don't get a
stray newline when the port is free.
2026-04-27 16:29:10 -04:00
DJP
0eedd412e6 Match /gsb/-style deploy: built SPA on disk + Apache Alias
The dev server runs everything else (/gsb/, /olivas/, /cc-dashboard/,
etc.) the same way: backend in Docker on a 127.0.0.1 port, frontend
built once and served by Apache as a static SPA via Alias. This commit
restructures V2 to match.

Apache (deploy/apache-osop.conf):
- Drops the ProxyPass-everything model; uses Alias + Directory + SPA
  RewriteRule, identical to the /gsb/ block.
- Stays as an Include — drop a single line into the merged vhost:
    Include /opt/oliver-sales-ops-platform/deploy/apache-osop.conf

Compose (docker-compose.yml):
- Frontend service moved behind a "dev" profile so it doesn't start in
  production. Locally:  COMPOSE_PROFILES=dev docker compose up
- Backend / db / redis are unprofiled and always come up.

Deploy script (deploy/deploy.sh):
- Sanity, port-conflict (5435/6380/8003 only — frontend port no longer
  needed in prod), git pull, docker compose build + up.
- New step 5: builds the Vite SPA in a one-shot node:20 container and
  rsyncs (or cp -a) the dist/ output to /var/www/html/oliver-sales-ops-platform/.
  Uses sudo if the dest isn't writable as the deploy user.
- Reports the Apache include line + reload command.
- New flag: --no-frontend (skip the build-and-sync step).
- Unsets COMPOSE_PROFILES so the dev profile never activates on prod.

Frontend type fixes (caught by `tsc && vite build`, hidden by dev mode):
- types/index.ts gains ClientAsset / Match / MatchConfidenceKey /
  RatecardLine / RatecardSummary, and StageArtifact gains its three
  cost columns. These were referenced from api/assets.ts and several
  Stage panels but were missing on the export side.
- New src/vite-env.d.ts declares ImportMetaEnv (VITE_DEV_AUTH_BYPASS).
- Drops one unused import (Stage4QAPack: downloadQAPack) and one
  unused variable (Stage16Delivery: currentBadge).
- Production build now passes: tsc clean, vite build clean, dist/
  index.html correctly references /oliver-sales-ops-platform/assets/...

Server layout (deploy server):
  /opt/oliver-sales-ops-platform/                — repo + compose
  /var/www/html/oliver-sales-ops-platform/       — built SPA
  Apache vhost includes deploy/apache-osop.conf
  Backend at 127.0.0.1:8003, db 5435, redis 6380 — none clash with the
  /gsb/ stack (5432/8002/3010) or any other listed in the merged vhost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:16:08 -04:00
DJP
b8dadb4f99 Deploy infra + admin user seed + URL path migration
Three things needed for the optical-dev rollout.

1) Path migration /osop/ → /oliver-sales-ops-platform/
   The public URL is https://optical-dev.oliver.solutions/oliver-sales-ops-platform/
   Updated the basename across:
   - Vite base + proxy match
   - React Router basename
   - axios baseURL
   - MSAL redirectUri (preserves SSO when wired)
   - downloadQAPack URL
   - Backend app_path_prefix default
   - docker-compose APP_PATH_PREFIX default
   - Apache Location blocks

2) DEV_AUTH admin user
   Auth middleware now reads DEV_AUTH_EMAIL / DEV_AUTH_NAME / DEV_AUTH_ROLE
   when DEV_AUTH_BYPASS=true (defaults preserve the old dev@localhost /
   editor behaviour). The dev_bypass identity also promotes existing
   AppUser rows to admin if the env says so — so no manual SQL on the
   server when we want a different account exposed.
   New backend/scripts/seed_admin.py is idempotent and runs from
   start.sh after Alembic migrations. It upserts the configured
   DEV_AUTH_EMAIL with role=admin (or whatever DEV_AUTH_ROLE says).
   Smoke-tested locally: /api/users/me now returns
   admin@oliver.agency / role=admin.

3) Deploy assets under deploy/
   - apache-osop.conf — drop-in vhost block (Location /…/api/ → 8003,
     Location /…/ → 3011, ProxyTimeout 300, redirect bare prefix to /).
     Sits alongside the existing /gsb/ V1 block on the same vhost.
   - deploy.sh — idempotent script:
     * sanity (.env present, docker on PATH)
     * port-conflict check (5435 db, 6380 redis, 8003 backend, 3011
       frontend) — if our compose project is already running, skips
       the lsof check because the ports are ours; otherwise warns and
       exits if anything else is listening
     * git pull --ff-only (skip with --no-pull)
     * docker compose build && up -d (skip with --no-build)
     * health-poll backend /api/health for up to 60s
     * frontend probe at the new path
     * prints local + public URLs and the admin email on success

V1 host ports (5432/8002/3010) and V2 host ports (5435/6380/8003/3011)
are non-overlapping by design, so both stacks coexist on the same dev
server. CLAUDE.md naming policy is satisfied — docker-compose.yml has
name: oliver-sales-ops-platform pinned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 16:08:50 -04:00
DJP
065a52c8fd Fix Stage 2 empty clarifications + Stage 13 missing SLAs/KPIs
End-to-end test (a243d6fa) on a deliberately tricky brief surfaced two
silent failures in the agent layer:

1. Diagnose Agent stuffed every clarification into the ambiguities[]
   field and returned clarifications=[]. Stage 5 then had nothing to
   ingest. Fix:
   - clarifications gets minItems=6 / maxItems=25 in the tool schema.
   - System prompt now distinguishes ambiguities (observations) from
     clarifications (the actual *questions* to ask the client) and
     mandates that every ambiguity must reappear here as a question-
     marked sentence.
   - max_tokens 4096 → 8192 (the schema is heavier now).

2. Support Docs Agent only returned summary/caveats/assumptions —
   slas and kpis were truncated. The fields existed in the schema but
   weren't required, max_tokens=3072 was the bottleneck. Fix:
   - slas and kpis get minItems=3, kpis added to required[], schema
     descriptions made explicit ("MUST be populated").
   - max_tokens 3072 → 8192.

Verified on the Versuni opportunity:
  Diagnose: clarifications_seeded 0 → 15.
  Support docs: slas/kpis/governance 0/0/0 → 11/11/11.

Two pre-existing observations from the same E2E run (intentionally NOT
patched, documented in the test agent's report):
- Stage progression is lazy — agents accept calls regardless of stage
  state; only stages/N/complete enforces ordering. This is by design
  for now (lets users iterate out of order before "closing" a stage).
- Cost stamps were $0 for older artifacts (pre-cost-tracking commit
  b41e399). Re-running any agent overwrites with a properly-stamped
  artifact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:36:41 -04:00
DJP
212b19d255 Stages 15 + 16: Claude Pitch Deck Agent + post-win Delivery Planning
Stage 15 — Pitch Deck Agent
Backend:
- New service pitch_deck_agent.py: Claude reads every upstream artifact
  (intake, diagnosis, qualification, delivery model, capability gaps,
  support docs) plus the normalized assets list, and produces a
  slide-by-slide outline via the submit_pitch_deck_outline tool. Each
  slide has section, title, key_points (3-6 bullets), speaker_notes,
  and an optional data_callout for headline numbers.
- Cost stamped on the artifact like every other Claude agent.
- POST /opportunities/{id}/pitch-deck — runs the agent.
- GET /opportunities/{id}/pitch-deck/outline-markdown — renders the
  saved outline to a printable markdown doc.
- The original pitch_deck_stub markdown route is preserved as a
  "quick auto-composed" fallback.

Frontend (Stage15Pitch):
- Headline + deck_summary cover panel.
- Slide-by-slide outline with per-section colour coding (cover /
  context / approach / scope / team / commercials / governance /
  next_steps), data callouts surfaced as bordered chips, speaker
  notes collapsed into an italic block per slide.
- Two download buttons: structured outline markdown, and the legacy
  auto-composed quick deck.

Stage 16 — Implementation Plan (post-win)
Backend:
- New service implementation_plan_agent.py: Claude composes a phased
  rollout (3-6 phases with timeframe, objectives, milestones, owner),
  per-market rollout sequence, training & adoption items, compliance &
  policy items, in-flight metrics, and risks.
- Hard-gated: raises ValueError unless deal_status is WON, mapped to
  400 by the route. Increased max_tokens to 8192 because the first
  pass hit 4096 and got truncated.
- POST /opportunities/{id}/implementation-plan.

Frontend (Stage16Delivery):
- Deal-status switcher (Active / Won / Lost / Deprioritized) at the
  top, persists via PUT /opportunities/{id}.
- Active: prompt to mark Won or Lost first.
- Won: implementation-plan agent runner + structured render of the
  result (summary, phases with owner badges, market rollout table,
  training/adoption, compliance, in-flight metrics, risks).
- Lost / Deprioritized: lessons-learned textarea (Phase 2 will wire
  the save endpoint — currently note-taking only).

Smoke-tested against opp #2 (Versuni): pitch agent produced "Versuni
2026 Always-On: A Hybrid AI + Craft Engine for Philips and Senseo
Across EMEA" headline + 8-slide outline; flipped deal_status to won
and the implementation plan returned a 6-week ramp / Q3-Q4 2026
fast-follower rollout / Q1 2027 steady-state plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:57:02 -04:00
DJP
b41e3999c6 Per-stage AI cost badges + Stage 10 efficiency UI + Stage 8 fix
Three things in one commit because they share the same plumbing.

1) PER-STAGE COST DISPLAY
Migration 0006 adds cost_usd / input_tokens / output_tokens columns to
stage_artifacts. Every Claude-driven agent now stamps its own run cost
on the artifact it produces:
  - intake_agent (Stage 1)
  - diagnosis_agent (Stage 2)
  - asset_normalizer (Stage 6)
  - ai_matching (Stage 7) — accumulates across the per-asset loop and
    persists a NEW summary artifact (artifact_type='matching_run')
    with the run totals + assets_matched + matches_created counts
  - delivery_model_agent (Stage 9)
  - capability_gap_agent (Stage 12)
  - support_docs_agent (Stage 13)
A new <AgentRunCost> component renders a compact pill (label, cost,
in/out tokens) sourced from the most-recent stage artifact. Embedded
in the header of every Claude-driven stage panel: 1, 2, 6, 7, 9, 12, 13.
Per-deal cumulative cost still on the Stage 8 stats card.

2) STAGE 10 FRONTEND
Wires the Stage 10 efficiency-profile endpoint shipped in 2eb0422 to
a real UI (was placeholder before). Scenario picker (conservative /
moderate / aggressive), blanket slider, per-discipline overrides
(disabled when blanket > 0), tools-applied chips, free-text notes,
live impact preview that hits team-shape with the active settings,
and a Save button that persists to the artifact. Hydrates on mount
from the most recent saved profile.

3) STAGE 8 EMPTY STATE
"No ratecard yet" is now smart: when there ARE selected matches at
Stage 7 it surfaces the count + a primary "Build ratecard from N
matches" action, instead of telling you to do something you've
already done.

Smoke-tested: opp #2 ratecard rebuilt (35 lines); Stage 9 delivery
agent re-run shows $0.0437 / 4994 in / 1915 out on the badge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:38:50 -04:00
DJP
96b8e2dc3d Stage 15 stub: pitch deck markdown export
Phase 1 stub for the proposal deck. Pulls every upstream artifact
together into a 5-section markdown doc:

  1. The opportunity at a glance — intake summary + brands + delivery model
  2. What's being asked for — channels, markets, capabilities, deliverables, ambiguities
  3. Why we're a fit — qualification % + recommendation + delivery model + per-stage split
  4. Commercial framing — caveats, assumptions, KPIs, capability gaps
  5. AI cost so far — running total + call count

GET /opportunities/{id}/pitch-deck/markdown returns the .md file with the
right Content-Disposition. Saves a stage_artifact (type='pitch_deck_markdown').

Phase 2 will swap this for a python-pptx deck assembled from a template
library (the per-section content stays the same — just the rendering
changes).

Smoke-tested: 6.5KB doc on the Versuni opportunity, all 5 sections
populated from real artifacts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:24:30 -04:00
DJP
2eb0422e40 Stage 10: persist efficiency profile as stage artifact
Stage 10 was previously implicit — the per-discipline override sliders in
Stage 11 fed straight into team-shape calculations without leaving an
audit trail. This adds:
- POST /opportunities/{id}/efficiency-profile — saves the active
  scenario / blanket pct / discipline overrides / tools applied / notes
  as a stage_artifact (type='efficiency_profile').
- GET /opportunities/{id}/efficiency-profile — returns the most recent.

The payload shape is loose by design ({scenario, blanket_pct,
discipline_overrides, tools_applied, notes}) so the Stage 11 UI can
evolve without a migration. The artifact is the audit trail; the live
calculation still runs on the query params passed to GET /team-shape.

Smoke-tested against opp #2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:21:35 -04:00
DJP
7c5ab130ed Stage 13: Support Docs agent (caveats / SLAs / KPIs / governance)
Generates the support docs that anchor the proposal so sales promises
match delivery reality. Reads the upstream artifacts (Stage 2 diagnosis,
Stage 9 delivery model, Stage 12 capability gaps) and produces a
structured set of caveats, assumptions, SLAs (V1/V2/V3 turnaround days +
responsible party), KPIs (metric / target / measurement), governance
clauses, and a one-paragraph framing summary.

The system prompt bakes in the PARASOL Studio SLA defaults (24-48h brief
ack, 2 rounds standard, V1/V2/V3 day patterns per asset class) and
default scope exclusions (third-party fees, talent, music licensing,
shoot/production). Phase 2 will switch to a real template_library
sourced from PARASOL_Studio_SLAs_2024_V1.pptx; Phase 1 generates from
scratch with those defaults baked into the prompt.

Saved as stage_artifact (type='support_docs').

Frontend (Stage13SupportDocs):
- Run / re-run agent button.
- Summary card.
- Caveats / assumptions / governance list panels (colour-coded).
- SLA table (Deliverable / V1 days / V2 days / V3 days / Owner / Notes).
- KPI cards (Metric / Target / Measurement).

Smoke-tested against the Versuni opportunity: agent produced caveats
including "Pricing assumes 200 social posts/month and 80 eCom
assets/month as gross output volumes; retailer-specific variants
beyond 2 cuts per eCom asset…" — pulling forward concrete numbers from
the upstream stages exactly as designed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:19:12 -04:00
DJP
18201a22e5 Stages 9 + 12: Delivery Model + Capability Gaps agents
Stage 9 — Delivery Model (services/delivery_model_agent.py)
- Reads the Stage 2 diagnosis and recommends headline traditional /
  AI-supported / hybrid + a per-workflow-stage breakdown (manual /
  ai_supported / fully_automated) with tooling and rationale, plus
  tooling caveats and risks.
- Tool capability cheat-sheet baked into the system prompt: Pencil
  (statics only), Creative-X (brand QC), Semblance (video edits),
  OMG (DCO), Google Vids/Synthesia (simple internal motion),
  origination/print/TVC stay manual. Agent doesn't oversell AI.
- Saved as stage_artifact (type='delivery_model').

Stage 12 — Capability Gaps (services/capability_gap_agent.py)
- Reads the Stage 2 diagnosis and returns: core_in_scope (OLIVER's own
  capability list), gaps (capability + criticality red/amber/green +
  suggested_source internal_sme/brandtech_partner/external_vendor +
  named partner suggestion + rationale), summary.
- Suggested partners are the actual Brandtech-group + external pattern:
  Jellyfish for SEO, Gravity Road for social strategy, external prod
  cos for TVC at scale, etc.

Both agents share a `_run_simple_agent` helper in api/opportunities.py
that handles the validation/ commit / artifact-id-return pattern.

Frontend:
- Stage9DeliveryModel: headline badge (TRAD / AI-SUPPORTED / HYBRID) +
  per-stage cards with approach colour + tooling chips, plus tooling
  caveats and risks panels.
- Stage12CapabilityGaps: core-in-scope chips, gaps list with
  criticality badge + source label + named partner.

Smoke-tested: hybrid recommendation for Versuni with 7 workflow stages
breakdown; capability gaps spotted EMEA transcreation (amber, external
vendor) on top of OLIVER's core in-scope list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:15:28 -04:00
DJP
21aecff0cb Stage 11: Team Shape (FTE) + 27 new tests for Stages 6/7/8
Stage 11 backend (services/team_shape.py + api/team_shape.py):
- Ports V1's calculate_team_shape with the bug-4 fix already applied:
  total_hours / manual_override on RatecardLine are per-1-asset; the
  aggregator multiplies by line.volume.
- Programme roles never see efficiency cuts. Per-discipline efficiency
  is capped at MAX_EFFICIENCY (90%).
- GET /opportunities/{id}/team-shape supports two modes:
  * `efficiency_pct=N` blanket reduction across delivery roles, OR
  * `discipline_overrides=<JSON>` per-discipline rates (Stage 10
    efficiency profile output).
- Smoke-tested: 5,620.5 hrs / 3.12 FTE on the Versuni opportunity;
  applying efficiency_pct=50 correctly halves to 2,810.25 hrs / 1.56 FTE.

Stage 11 frontend (Stage11TeamShape.tsx + api/teamShape.ts):
- Stats card (total / delivery / programme / adjusted FTE).
- Blanket efficiency slider 0–90% (disabled when per-discipline mode is
  active so the two modes don't fight).
- One slider card per discipline showing live percentage; clearing all
  reverts to the blanket slider.
- Per-discipline FTE table with Total hrs / FTE / Eff% / Adjusted hrs /
  Adjusted FTE columns. Programme roles tagged with a badge.

Stage 6/7/8 backend tests (parallel test agent, +27 tests):
- test_assets.py (10): CRUD + sort_order auto-increment + cascade
  delete + 400/404 paths + skipped real-Claude normalize.
- test_matching.py (10): 400 on no-assets, GET shape + ordering,
  selection toggle deselects siblings, deselect leaves siblings alone,
  cross-opp 404, skipped real-Claude run.
- test_ratecard.py (7): 400/404 paths, end-to-end build+get with the
  bug-4 invariant explicitly asserted (line.base_hours == total_hours;
  summary.total_hours == sum(base × volume)), no-selection skip,
  rebuild idempotence, two-asset volume aggregation splits 1:2.

Suite: 100 collected, 95 passed, 5 skipped (all real-Anthropic), 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:10:48 -04:00
DJP
a5a50e1dd3 Stages 6/7/8 frontend: Normalize / Match / Ratecard panels
Stage 6 — Asset Normalizer panel
- "Run normalizer" button (with destructive-action confirm because
  re-running cascades to matches + ratecard).
- Inline edit / delete / volume / tier per ClientAsset row.
- Manual "+ Add asset" form for cases where the agent missed something.

Stage 7 — Match panel
- Per-asset card showing all candidate matches (top 3 within 5% of the
  best score). Radio-button selection enforces one selection per asset
  via PUT /matches/{id}/select (backend deselects siblings).
- Confidence badge (exact / close / multiple / none), 0-1 score, AI
  reasoning, and any caveats are surfaced inline so the user can choose
  with the same context Claude saw.
- "Run matching" kicks off the background task; the matches query
  refetches every 4s while we're polling and stops automatically once
  every asset has at least one candidate.

Stage 8 — Ratecard panel
- Stats row (assets priced, lines, total hours, AI cost so far on the
  opportunity).
- Per-asset summary table (asset / volume / role count / total hours).
- Hours-by-line table making the bug-4 fix legible: separate Base hrs,
  Vol, and Total hrs (= base × vol) columns so the per-1-asset hours
  are visible alongside the project-effort total.
- Build / Re-build button; the API call invalidates the ratecard +
  opportunity caches so the cost meter refreshes.

API hooks and types added under api/assets.ts and types/index.ts:
useClientAssets, useCreateAsset, useUpdateAsset, useDeleteAsset,
useRunNormalize, useMatches (with optional polling), useKickOffMatching,
useSelectMatch, useRatecard, useBuildRatecard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:07:16 -04:00
DJP
1581256823 Stages 6/7/8 backend + Stage 3/4 tests: V1 core ported
Migration 0005 adds three new tables (and the matchconfidence enum):
client_assets, matches, ratecard_lines. All FKed to opportunities with
ondelete=CASCADE so re-running a stage cleanly wipes the downstream
artifacts that depended on it.

Stage 6 — Asset Normalizer:
- services/asset_normalizer.py runs Claude (full uploaded text + a hint
  from the Stage 2 diagnosis) and submits a structured deliverables
  list via tool_use.
- api/assets.py exposes CRUD on ClientAsset rows + POST .../normalize
  to (re-)run the agent. Re-running wipes existing assets, which
  cascades to matches and ratecard_lines automatically.
- Smoke-tested against the Versuni brief: 7 normalized assets seeded.

Stage 7 — AI matching:
- services/ai_matching.py is the V1 engine ported and slimmed for V2:
  full GMAL catalog sent per call (~3k–20k tokens depending on whether
  AI-enhanced descriptions are populated), Claude returns up to 3
  candidates, top match auto-selected when score >= 0.8, alternatives
  kept only when within 5% of the top score.
- BackgroundTasks runs the agent off-request so the frontend can poll
  GET /matches for progress. Per-call AI cost rolls onto the opportunity.
- api/matching.py: POST /match (kick off), GET /matches, PUT
  /matches/{id}/select (toggle the chosen one — auto-deselects siblings
  so there's exactly one selection per asset).
- Smoke-tested: 15 matches across 7 assets, GMAL323 et al picked with
  reasonable scores.

Stage 8 — Ratecard:
- services/ratecard_builder.py is the V1 builder, with the V1 hours×
  volume bug-fix already baked in: total_hours stores per-1-asset
  hours; volume sits on the row; aggregators multiply at read time.
- api/ratecard.py: POST /ratecard/build, GET /ratecard returns a
  RatecardSummary whose total_hours is correctly computed as
  sum(per_asset × volume).
- Smoke-tested: 24 lines, 5,620.5 total project hours, base × volume
  displayed separately.

Tests added by the parallel test agent (commit was queued during build):
- test_qualification.py (10): TROWLS save/get, threshold boundaries
  (50%/60%), Pydantic 0–10 range guards, missing-dimension 422,
  qualification_score stamped on opportunity, newest-scorecard wins,
  404 paths.
- test_qa_pack.py (5): Excel + Word downloads (real PK\\x03\\x04
  signature, openpyxl-parseable, priority-sort verified), filename
  safety against /, ?, ", 404 paths.

Suite: 73 collected, 70 passed, 3 skipped (real-Anthropic), 0 failed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:03:13 -04:00
DJP
2abd0892fd Stage 4 + Stage 5: Q&A pack export + answer ingestion
Stage 4 — backend exports the clarification questions table as either
.xlsx (openpyxl, with priority colour-coded column + free-text answer
column) or .docx (python-docx, grouped by category, RED→AMBER→GREEN
inside each group, with rationale and a labelled answer slot per
question). Both include an opportunity title, client/region header
and a generation timestamp.

Stage 4 — frontend renders the existing clarification list with summary
stats (total / red / amber / green / answered) and two download buttons
that fetch via the authed axios client (so dev bypass + MSAL tokens
behave the same as every other request) then trigger a Blob download.
Buttons are disabled when there are zero clarifications.

Stage 5 — pure frontend (no new endpoints needed; reuses
PUT /clarifications/{id}). Per-question editor with a textarea for the
client's answer, save button (disabled when text is unchanged), and
quick status buttons (Mark answered / Dismiss / Reopen). Stats card
tracks pending / answered / dismissed and a "reds resolved" counter
since RED items are the actual blockers for Stage 6 (Normalize).

Smoke tests:
- Excel: HTTP 200, 7,445 bytes, valid Microsoft Excel 2007+ file.
- Word: HTTP 200, 38,823 bytes, valid OOXML.
- Both render the 15 Versuni clarifications from earlier sessions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:53:05 -04:00
DJP
c9be450614 Stage 3: TROWLS qualification scorecard + Stage 2 diagnosis tests
Stage 3 backend:
- POST /opportunities/{id}/qualification — saves a TROWLS scorecard
  (timing/relationship/opportunity_size/what_we_know/location/sector,
  each 0-10) as a stage_artifact (type='qualification_scorecard'),
  computes total/60 → percentage, derives recommendation
  (proceed >=60% / slt_review >=50% / no_go <50%) and stamps
  qualification_score on the opportunity.
- GET /opportunities/{id}/qualification — returns the latest scorecard
  or null. Smoke-tested: scores 7+8+6+7+9+7=44 → 73% → "proceed".

Stage 3 frontend (Stage3Qualify component):
- Live total + percentage card, three-band progress bar with markers
  at 50% (SLT review) and 60% (proceed), live recommendation badge.
- One slider + one note input per TROWLS dimension, plus an overall
  notes textarea.
- Save scorecard, with success indicator. Re-loads existing score on
  mount so re-edit is non-destructive.
- StageApprovals embedded below — qualification gate uses the same
  approval workflow the rest of the gated stages use.

Stage 2 diagnosis backend tests (10 new, +1 skipped real-Claude):
- /diagnose: no-files → 400, empty extracted_text → 400.
- /clarifications: empty list, priority ordering (red→amber→green
  verified by seeding out-of-order via psycopg2), 404 for non-existent
  opp, cross-opportunity 404 on PUT.
- PUT clarification: client_answer flips status→answered + stamps
  answered_at; status=dismissed doesn't stamp answered_at; bad status
  → 400.

Suite total: 58 collected, 55 passed, 3 skipped (real-Anthropic +
owner-notification path that needs OpportunityCreate to accept
owner_user_id). 0 failures, no app-code bugs found.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:48:44 -04:00
DJP
513cf10a1e Stage 2 frontend: Diagnosis Agent panel + clarification list
Surfaces what the Diagnosis Agent already produces on the backend.

Stage2Diagnose component renders:
- Run/Re-run diagnosis button (disabled until at least one extractable
  Stage 1 file exists).
- Diagnosis summary card with complexity assessment.
- Deliverables table (name / category / volume / complexity hint).
- Chip panels for channels, markets, capabilities, KPIs/SLAs, tech
  asks, timelines.
- Highlighted ambiguities + contradictions panels.
- Clarification questions list (priority badge, category, question,
  rationale, status, client answer if present) — these are the rows
  Stage 4 will package into a Q&A pack.

Three new TanStack Query hooks: useRunDiagnosis, useClarifications,
useUpdateClarification (the last unused for now — Stage 5 will wire it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:45:23 -04:00
DJP
b8baf1a37e Tests: approval workflow + notifications (13 new, 46 total)
Extends the integration suite with coverage for the approval and
notification endpoints shipped in 3f7531a / f631dad.

test_approvals.py (9 tests):
- /api/users/me returns the dev-bypass user; /api/users includes them.
- Requesting approvals on a non-gated stage returns 400 naming the
  gated stages.
- Unknown approver_user_id returns 400.
- Happy path: request -> list -> /approvals/me -> blocked stage 3
  complete -> approve -> stage 3 completes, opportunity advances.
- Re-deciding the same approval returns 400.
- Reject path still blocks stage 3 complete with the same error pattern.
- Token deeplink path: GET /approvals/by-token/{token} returns the
  context; bogus token -> 404.
- Invalid decision string ('maybe') -> 400.

test_notifications.py (4 active + 1 skipped):
- APPROVAL_REQUESTED notification surfaced after request_approval.
- unread-count delta after mark-one-read.
- POST /me/mark-all-read zeros the count.
- Owner-notification path is skipped with a clear marker — the create
  endpoint doesn't yet accept owner_user_id and the service correctly
  skips self-notification when approver == owner.

Notes:
- Test harness reuses the existing `client` and `opportunity` fixtures.
- Cascade-delete (verified via 0001_initial.py / 0004_notifications.py)
  means deleting the opportunity in the fixture teardown also cleans
  approvals + notifications, so no extra teardown is needed.
- Email tokens aren't exposed in the API; the test reads them via
  psycopg2 against localhost:5435 (DSN overridable via OSOP_TEST_DSN).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:32:25 -04:00
DJP
f631dad00b Approval workflow frontend: bell + StageApprovals + /approvals/:id
Brings the approval flow into the UI end-to-end.

Nav:
- NotificationBell with live unread-count badge (polled every 60s).
  Click to open a popover listing the latest 50 notifications;
  unread items are tinted with the OLIVER accent. Clicking an item
  marks it read and routes to its link_path. "Mark all read" empties
  the unread state in one call.

Gated stages (3 = qualification, 14 = approval gates):
- StageApprovals component embedded in those stage views. Empty state
  prompts the user to request an approval. Form: pick role + approver
  from the directory (loaded from /api/users — populated automatically
  as people log in). Submitting fires the backend, which creates the
  approval, queues the in-app notification and the Mailgun email
  (skipped silently in dev). Existing approvals render as a list with
  status badges, requested-at + decided-at timestamps, comment text,
  and a deeplink to the approval page. A summary line at the bottom
  tells the user when all approvals are in (and whether they all
  approved or any rejected).

Approval page:
- New routes /approvals/:id and /approvals/by-token/:token (the email
  link path) hit the same component. Shows opportunity context (client,
  region, deadline, summary), the approval's status with timestamps,
  prior decision notes if any, and — for the assigned approver or any
  admin — an Approve / Reject form with a notes textarea. Reject is
  disabled until notes are filled in (gentle nudge to give a reason).
  After submitting, TanStack Query invalidations refresh the bell, the
  stage approvals list, and the opportunity stages so the user sees
  the update without a manual reload.

Wiring:
- Three new TanStack Query modules: api/approvals.ts (stage list, mine,
  by id, by token, request, decide), api/notifications.ts (list,
  unread-count w/ refetch interval, mark read, mark all), api/users.ts
  (directory + me).
- types/index.ts gains Approval, Notification, UserBrief, APPROVAL_ROLES.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:32:05 -04:00
DJP
3f7531a1bf Approval workflow backend: notifications + Mailgun email
The two gated stages (3 = qualification, 14 = approval gates) now have a
real workflow. An admin / orchestrator picks approvers, the backend creates
Approval rows, fires in-app Notifications, and sends a Mailgun email with
a deeplink to the approval page. Each approval gets a unique email_token
so links are per-recipient. When the approver submits a decision the
opportunity owner is notified back.

End-to-end smoke test on the Versuni opp:
1. POST /opportunities/2/stages/3/approvals — created Approval #1,
   notification queued, Mailgun "skipped" (no API key in dev) and logged.
2. POST /opportunities/2/stages/3/complete blocked: "pending approvals
   from commercial".
3. POST /approvals/1/decision {"decision":"approve",...} — status 'approved'.
4. POST /opportunities/2/stages/3/complete — succeeds, advances to stage 4.

Schema:
- New table 'notifications' (user_id, type enum, title, body, related FKs,
  read flag, indexes on user_id+read and user_id+created_at).
- Approvals table gets email_token (unique), email_sent_at, email_to.
- Migration 0004 (filename + revision id kept short — alembic_version is
  varchar(32), names longer than that crash the migration runner).

Services:
- mailgun.send_email — gracefully no-ops when MAILGUN_API_KEY is empty,
  logging the would-be payload so dev environments work without creds.
  Selects api.eu.mailgun.net when MAILGUN_REGION=eu.
- approval_service.request_approval — creates approval + notification +
  fires email, all in one call. Email is best-effort (logs on failure
  but doesn't roll back the approval).
- approval_service.record_decision — flips status, stamps decided_at,
  notifies the opportunity owner if there is one.
- notification_service.create_notification — thin helper.

Auth middleware now upserts the AppUser on every authenticated request
(including the dev bypass), so logged-in users automatically appear in
the approver directory at /api/users.

API:
- POST /opportunities/{id}/stages/{n}/approvals (only stages 3, 14)
- GET /opportunities/{id}/stages/{n}/approvals
- GET /approvals/me — my pending approvals
- GET /approvals/{id} — context (approval + opportunity summary)
- GET /approvals/by-token/{token} — same context, opened by email link
- POST /approvals/{id}/decision — {decision: 'approve'|'reject', comment}
- GET /notifications/me, /me/unread-count, PUT {id}/read,
  POST /me/mark-all-read
- GET /users, /users/me

Config:
- New env vars: MAILGUN_API_KEY, MAILGUN_DOMAIN, MAILGUN_FROM,
  MAILGUN_REGION, APP_PUBLIC_URL, APP_PATH_PREFIX (used to build
  email links). Plumbed into docker-compose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:28:22 -04:00
DJP
5ad6d01846 Add pytest integration test harness for backend
Test harness hits the live backend on http://localhost:8003 (override via
OSOP_BASE_URL) rather than spinning an in-process FastAPI app, so we test
the actual deployed surface. 33 tests pass, 1 intentionally skipped
(real Anthropic-backed intake — gated behind @pytest.mark.requires_anthropic
to avoid spending money on every run).

Coverage:
- Opportunity CRUD: create with defaults + invalid model_type, all 5
  model_type values round-trip, list/get/update, full create-delete-404.
- Stage machine: 17 rows initialised, advance unlocks next, double-complete
  rejected, out-of-order rejected, out-of-range rejected, notes persist.
- Stage gating: stage 3 without approvals returns 400 with "approval"/
  "gated" in the detail and does not mutate state.
- File upload: .txt, .md, synthesized .docx, synthesized .xlsx all
  upload + extract; .exe rejected; list/delete round-trip.
- Schema sanity: list endpoints return the expected shape.

Re-run:
  python3 -m venv /tmp/osop_test_venv && \
    /tmp/osop_test_venv/bin/pip install -q -r backend/requirements-dev.txt && \
    cd backend && /tmp/osop_test_venv/bin/pytest tests/ -v

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:22:32 -04:00
DJP
033bc106fe Stage 2 backend: Diagnosis Agent + clarification questions
Stage 2 reads the same files Stage 1 ingested and produces a structured
brief diagnosis (deliverables, channels, markets, capabilities, KPIs/SLAs,
tech asks, timelines, ambiguities, contradictions) plus a list of
clarifications to send the client. Diagnosis is saved as a stage_artifact
(type='brief_diagnosis'); clarifications land in a new
ClarificationQuestion table that Stage 4 will package into a Q&A pack
and Stage 5 will mark answered.

New table (migration 0003): clarification_questions with priority enum
(red/amber/green) and status enum (pending/answered/dismissed),
indexed by opportunity_id and (opportunity_id, status). source_stage
distinguishes Stage-2-seeded questions from Stage-5/manual additions so
re-running the Diagnosis Agent only wipes its own seeded rows, not
client answers already on file.

API: POST /diagnose (run agent), GET /clarifications (list, ordered by
priority), PUT /clarifications/{id} (used by Stage 5 to record answers).

Smoke-tested against the Versuni sample brief: 6 deliverables, 4
channels, 15 clarifications seeded, $0.0107 per call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:22:13 -04:00
DJP
ad571e8c48 Stage 1 part 2: file upload + Intake Agent
Stage 1 is now fully usable end-to-end. Users drop RFP/brief docs onto an
opportunity, run the Intake Agent, and the extracted metadata is stamped
back onto the opportunity (only filling fields that were left blank, so
manual entries are never overwritten) and saved as a stage_artifact for
later auditability.

Backend:
- New OpportunityFile model + migration 0002 (file metadata + cached
  extracted text + extraction error if parsing failed). Files land on
  disk under DATA_DIR/uploads/opp_<id>/ — the DB row is the index.
- text_extractor service supports .docx (python-docx), .xlsx/.xlsm
  (openpyxl), .pdf (pypdf, newly added), .txt/.md.
- intake_agent: Claude Opus 4.7 with submit_intake_metadata tool_use,
  150k-char input cap, ISO date conversion, conservative (omit rather
  than guess). Per-call AI usage rolls up onto Opportunity counters so
  the dashboard cost meter reflects per-deal spend.
- POST /opportunities/{id}/files (multi-upload), GET, DELETE,
  POST /opportunities/{id}/intake, GET .../stages/{n}/artifacts.
- Smoke-tested against a sample Versuni brief: extracted client, region,
  brands, service types, deadline, go-live, and a clean summary.

Frontend:
- Stage1Intake component: drag-and-drop + browse upload, file list with
  size and extracted-char count, per-file delete, "Run intake agent"
  action, metadata card showing client/region/brands/service-types/dates
  and a summary block.
- TanStack Query mutations invalidate the opportunity detail and stage-1
  artifact cache so the metadata card and the opportunity header refresh
  immediately after the agent finishes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:02:26 -04:00
DJP
1959f6c224 Stage 1 part 1: Opportunity CRUD + 17-stage state machine
Creating an opportunity now initialises all 17 StageState rows in a single
transaction, with stage 1 in_progress and stages 2-17 not_started. The
stage_machine service enforces sequencing (stage N can only be completed
once predecessors are completed) and the gating rule for stages 3 and 14
(at least one Approval row, all approved, before completion is allowed).

Backend: GET/POST/PUT/DELETE /api/opportunities, GET /stages,
POST /stages/{n}/complete. The opportunity serialiser maps SQLAlchemy
enum values back to lowercase strings so the response model coerces
cleanly.

Frontend: Dashboard listing opportunities (with deal_status badge and
current-stage hint), NewOpportunity form, OpportunityView with a
horizontal StageStepper component. Stepper renders all 17 stages with
status-driven visual states (locked / in-progress / completed / awaiting
approval), highlights the gated stages (3 and 14), and links into a
per-stage view via /opportunities/:id/stage/:n. The old Home page (the
Mermaid flowchart + stage card grid) is preserved as /about.

Wired through React Router; TanStack Query handles the cache + mutation
invalidation so the stepper updates immediately after a stage completes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 12:57:11 -04:00
DJP
5259032b22 Port GMAL ingestion + browse endpoints from V1
The full GMAL catalog now ingests cleanly into V2 — verified counts match
V1: 390 assets / 243 with hour routes / 120 roles / 8,660 hour records /
695 service lines / 165 role-level mappings.

The parser is the V1 implementation, narrowed to clear only GMAL-owned
tables (V1 also blew away projects/matches/ratecard_lines, which V2
doesn't own — opportunities live in their own state-machine tables).

Browse endpoints (/api/gmal/{assets,assets/{id},assets/{id}/family,roles,
stats}) are ported from V1 unchanged. Editor write endpoints and AI
description regeneration are deferred until the GMAL Editor UI lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 12:38:00 -04:00
DJP
7c607f5fa6 Scaffold V2: 17-stage state machine, Alembic, MSAL, Mermaid stage map
End-to-end runnable skeleton for the OLIVER Sales Operations Platform —
the V2 of the Scope Builder, broadened to a complete RFP-to-mobilization
pipeline (intake → qualification → Q&A → match → ratecard → delivery
model → efficiency → team shape → caveats → approval gates → pitch →
post-win planning → downstream handoff).

Backend (FastAPI + async SQLAlchemy + Alembic):
- Models: app_users (with workflow_roles for approver routing), GMAL
  catalog ported from V1 (gmal_assets / roles / gmal_hours / service
  lines / role-level mappings), and the new state machine
  (opportunities, stage_states, stage_artifacts, approvals).
- Initial Alembic migration creates 11 tables and 5 enum types using the
  postgresql.ENUM(create_type=False) pattern so the types aren't
  double-created when referenced from multiple columns.
- Claude client defaults to claude-opus-4-7 with cost tracking + debug
  log; Azure SSO middleware ported as-is from V1.
- Public /api/health round-trips a SELECT 1 to verify the DB is reachable.

Frontend (React 18 + Vite + TanStack Query + MSAL + Mermaid):
- Home page renders the canonical 17-stage flowchart (Mermaid) plus an
  enumerated stage card grid with the two approval gates highlighted.
- React Router uses /osop basename to mirror the V1 /gsb/ deploy
  pattern; axios client targets /osop/api with MSAL token interceptor.

Compose:
- name: oliver-sales-ops-platform (so the project doesn't collide with
  the deploy-folder default per shared-server policy in CLAUDE.md).
- Ports 5435 / 6380 / 8003 / 3011 to coexist with V1 on the same host.
- Source mounts on backend (app/, alembic/, alembic.ini) and frontend
  (src/, configs) so dev iteration doesn't require rebuilds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 12:35:03 -04:00
Dave Porter
e1f2a0661b Initial commit 2026-04-27 15:59:14 +00:00