Commit graph

139 commits

Author SHA1 Message Date
nickviljoen
59a0b2408c Restructure CLAUDE.md docs: slim project-wide root, complete per-client coverage
Splits the monolithic CLAUDE.md (962 lines) into a slim project-wide root (211 lines)
plus per-client files. Auto-loaded context drops ~88% per session.

Changes:
- CLAUDE.md slimmed to project-wide essentials (architecture, auth, deployment, branch
  strategy, deploy scripts, prod troubleshooting, pre-session checklist). Adds explicit
  session-start convention pointing to CLAUDE_<CLIENT>.md for client-specific work.
  Updates client roster table to all 10 clients with profile counts.
- New CLAUDE_AXA.md: document-mode pipeline + axa_policy_document profiles
- New CLAUDE_DIAGEO.md: key_visual + packaging profiles, check inventories
- New CLAUDE_UNILEVER.md: profiles + zero-score logic for face/new visibility
- New CLAUDE_HONDA.md, CLAUDE_RANK.md, CLAUDE_GENERAL.md: stubs (clients use generic
  profiles only — kept for completeness and future expansion)
- backend/CLAUDE.md: stale 932-line duplicate replaced with 18-line redirect to root
  + backend-specific quick pointers

Per-client files (CLAUDE_LOREAL.md, CLAUDE_AMAZON.md, CLAUDE_BOOTS.md,
CLAUDE_DOW_JONES.md) unchanged — already had the right content.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:29:16 +02:00
nickviljoen
f5aaf8da24 Merge feature/dow-jones-tuning into develop: WSJ Static prompt tuning 2026-05-06 12:03:56 +02:00
nickviljoen
9acda38adc Merge feature/boots-ppack into develop: Boots Production Pack profile (multi-page document mode) + tuned prompts 2026-05-06 12:03:51 +02:00
nickviljoen
f493a0182a Merge feature/axa-document-mode into develop: document-mode QC pipeline (Phases 1, 3, 4, 5) 2026-05-06 12:03:42 +02:00
nickviljoen
3b76bf2c9c Tune WSJ Static prompts: cap whitelist, graphic headline, split-layout logo, 30% sizing cap
- wsj_capitalization_punctuation: explicit complete-sentence whitelist + soft-flag pattern for Rule 5 price formatting (price_spacing_correct / price_bolded_correct accept needs_manual_check, new price_formatting_caveat field)
- wsj_typography_hierarchy: graphic/illustrative headline awareness — large stylised serif price/number graphics are recognised as the display headline; surrounding sans-serif copy is correctly classified as subhead/body. Stylised price headlines exempt from the period rule.
- wsj_logo_compliance: horizontal logo placement allows anchoring to the copy block on split/asymmetric layouts; mandatory sizing assessment block with worked examples, score capped at 6/10 for logos exceeding 30% of longest side.

Validated on 3 WSJ-NY test assets across 3 iterations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:01:59 +02:00
nickviljoen
cec11f1f6a Tune Boots PPack prompts: superscript guard, ALL CAPS / logotype exceptions, weight/sizing limits
Three rounds of prompt tuning against the Remington (4p), Easter Overlay
(18p), and Grenade (7p) sample packs. Easter Overlay (the noisiest)
climbed 72.38 → 78.97 → 80.04 across iterations, with strict-grade
violations dropping 27 → 18 → 14. Remaining violations are now genuine
compliance issues — the noise patterns are cleared.

boots_caveat_compliance:
- Superscript guard: vision LLM was flagging every roundel asterisk as
  superscript because the * glyph naturally sits high in its line.
  Strict two-feature rule now required (raised baseline AND visibly
  shrunk ~50-60% of body). Borderline cases → "needs_manual_check"
  with new superscript_caveat field. Caveat avg 4.4 → 7.27.
- Same vision-LLM caveat applied to weight_matching (Light vs Regular
  at small sizes is below detection threshold) and sizing_compliant
  (1-2pt size differences below detection threshold). New weight_caveat
  and sizing_caveat fields. Reserved 1-2 score band for unambiguous
  critical violations only.
- Explicit scoring principle: "when in doubt, prefer 7-8 with
  manual_check flags over a lower confident-violation score".

boots_brand_name_accuracy:
- ALL CAPS retail convention now explicitly acceptable. L'OREAL,
  ESTEE LAUDER, MAYBELLINE etc. no longer flagged as casing errors —
  only structural element mismatches (accents, hyphens, apostrophes,
  special chars) count.
- Stylised brand logotype exception: known logomarks like `17` for
  SEVENTEEN, &SISTERS ampersand styling, e.l.f. dot rendering are
  Pass — surfaced via new logotype_observations field.
- Brand name avg 5.53 → 7.47 → 6.67 (LLM run-to-run variability).

Strongest real catch in dataset: Easter Overlay page 14 is labelled
for the ROI market in production notes but uses £ instead of € on
the artwork. Exactly the pre-press error worth surfacing. Caught
consistently across all runs by boots_currency_locale.

CLAUDE_BOOTS.md updated with three-pack smoke-test table, vision-LLM
limitations summary, and the four reusable prompt-tuning patterns
that worked on this build.

Local-only — feature/boots-ppack remains unmerged until after Boots
show-and-tell.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 16:26:11 +02:00
nickviljoen
50d0063b37 Add Boots Production Pack profile (multi-page document mode)
New profile boots_ppack for QCing multi-page Boots production packs
(PowerPoint-exported PDFs, 4-18 pages each). Built on top of AXA's
document-mode infrastructure — branched off feature/axa-document-mode
because it reuses the dispatcher, ingest, and result writer.

New checks:
- boots_logo_compliance — three-path scoring (master wordmark / partner
  lock-up / no branding) so OLIVER x BOOTS-style footer lock-ups aren't
  scored against master wordmark rules. Conservative without a formal
  Boots logo guideline.
- boots_colour_palette — verifies CMYK/RGB/Hex spec values on creative-
  guidance pages against canonical Boots Blue / Health Primary Blue /
  Offer Red, plus visual sanity-check on artwork pages.

Existing checks tuned:
- boots_brand_name_accuracy: closed-world list semantics. Brands not on
  the approved list now go to names_not_on_list (manual review) instead
  of failing — the list is sourced from the original 7 docs and is known
  incomplete (Remington, Imodium, Maybelline etc. are legitimate Boots-
  stocked brands not on it).
- boots_tandc_wording: explicit font-weight caveat — Boots Sharp Regular
  vs Light isn't reliably distinguishable by vision LLM at small sizes.
  Surfaced via font_weight_caveat field + needs_manual_check value.

Page classifier (document_mode/page_classifier.py):
Heuristic tags each page as cover / checklist / palette / notes /
artwork. Validated on all 10 sample packs.

Strict-grade exemption (Profile.strict_grade flag):
Only artwork-classified pages count towards Pass/Fail. Cover, checklist,
palette, and notes pages are still QC'd and reported as Informational
but cannot trigger a Fail. Banner shows exactly which artwork-page
checks fell below 6.

Result writer extended:
- Per-page table with score + page_type pill for any page_each-scope
  check (auto-applied as fallback)
- Strict-grade banner (red on violation, green when clean)
- Page_type pills throughout the per-page strip

Smoke-test result (Remington 4-page pack, 2026-05-05):
Overall 70.75/100, strict-grade Fail. After two iterations of prompt
tuning, all three remaining strict-grade violations are real catches:
orphan asterisk in T&Cs, "they may not be stocked" wording deviation,
missing "Charges may apply". brand_name_accuracy 7.0 (was 3.0 before
list fix), logo_compliance 9.5 (was 1.5 before lock-up path fix).

Local-only — not pushed to dev or merged to develop until after Boots
show-and-tell. Same posture as feature/axa-document-mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:47:13 +02:00
nickviljoen
90563b8cf2 Add AXA document-mode QC pipeline (Phases 1, 3, 4, 5)
Multi-page PDF QC for AXA Ireland policy documents. Runs as a third mode
alongside static + video, gated on profile.mode. New code isolated under
backend/document_mode/ with new endpoints under /api/document/*.

Phase 1 — Spine + 6 deterministic doc-scope checks ($0, runs in seconds):
- Scope-aware dispatcher (document/targeted/page_sample/page_pair/page_each)
- axa_font_inventory, axa_phone_inventory, axa_bold_words_definitions,
  axa_page_numbering, axa_print_code, axa_omg_versioning
- Bootstrap bold-words dictionary extracted from Example 1 General Definitions

Phase 3 — Old-vs-new diff (~$0.50/run, 3-5 min):
- Page alignment via difflib SequenceMatcher (windowed fuzzy match)
- Vision-LLM page-pair diff via Gemini 2.5 Pro (8 concurrent)
- Two-slot upload UX, axa_policy_document_diff profile, mode=document_diff

Phase 4 — PDF accessibility (PyMuPDF, $0):
- 9 PDF/UA-1 aligned criteria (tagged structure, /MarkInfo, title, /Lang,
  encryption, font embedding, PDF version, XMP UA-conformance, alt-text)
- _run_verapdf() stub for optional Java-based veraPDF integration later

Phase 5 — Print preflight (PyMuPDF, $0):
- 7 criteria (page geometry, bleed, image colour spaces, image DPI,
  transparency, PDF/X conformance, spot colours)

Profile additions:
- axa_policy_document — 8 deterministic checks, $0 cost
- axa_policy_document_diff — 1 page-pair LLM check, ~$0.50/run

API additions:
- POST /api/document/start_analysis (single PDF)
- POST /api/document/start_diff (old + new PDFs)

Frontend additions:
- Third profile.mode value (document_diff) in applyProfileMode()
- Two-slot upload UX with PDF-only file pickers
- checkFormValidity() branches by mode for the analyse-button gate

Smoke-tested locally against Example 1 (Home Insurance V8, 86pp) and
Example 2 (Landlord V1 vs V10, 68→74pp) with real findings caught
including bold-words gaps, missing PDF/UA flag, transparency on press,
V1→V10 bold-formatting fixes. Plan + integration map + gotchas in
backend/AXA_DOCUMENT_MODE_PLAN.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:38:14 +02:00
nickviljoen
67ed7fdd9d Add wsj podcast profile to Dow Jones client, File naming check added to all profiles 2026-04-29 18:17:36 +02:00
nickviljoen
b32e8f0c8b Add wsj podcast profile to Dow Jones client, File naming check added to all profiles 2026-04-29 18:09:58 +02:00
nickviljoen
24c716df77 Fix /api/access_request iterating list_access_entries() as a list
list_access_entries() returns a dict {default_clients, entries} but the
endpoint iterated it directly, which yields the dict keys (strings) and
then crashed on .get('is_admin') with "'str' object has no attribute
'get'". Read access_data['entries'] instead so admin recipients are
collected correctly and the request email actually sends.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 08:24:22 +02:00
nickviljoen
24ea62b082 Fix /api/access_request iterating list_access_entries() as a list
list_access_entries() returns a dict {default_clients, entries} but the
endpoint iterated it directly, which yields the dict keys (strings) and
then crashed on .get('is_admin') with "'str' object has no attribute
'get'". Read access_data['entries'] instead so admin recipients are
collected correctly and the request email actually sends.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 08:20:25 +02:00
nickviljoen
f17a4ed6da Box redirect URI: infer from hostname when X-Forwarded-Host is absent
The previous fix relied on Apache forwarding X-Forwarded-Host, but on
optical-dev that header isn't set. Apache uses ProxyPreserveHost (so
request.host correctly resolves to optical-dev.oliver.solutions) but the
backend connection is plain http and Flask sees no path prefix, so the
fallback emitted "http://optical-dev.oliver.solutions/auth/box/callback"
— which Box rejected as "insecure_redirect_uri" (no HTTPS) and which is
also missing the required /ai_qc/ prefix.

Resolution order is now:
  1. BOX_REDIRECT_URI env var (escape hatch / unusual deploys).
  2. X-Forwarded-Host header if Apache happens to send it.
  3. Otherwise: infer from request.host. Any host that isn't localhost
     or 127.0.0.1 is treated as the optical-dev / optical-prod proxy and
     gets HTTPS + the /ai_qc/ prefix. localhost stays http and rootless.

Verified all five paths (dev with and without XF-Host, laptop on
localhost and 127.0.0.1, explicit override) produce the right URL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:55:14 +02:00
nickviljoen
7c3945417a Compute Box OAuth redirect URI from the request
Caught a redirect_uri_mismatch on the dev server: the env file was the
localhost one (BOX_REDIRECT_URI=http://localhost:7183/auth/box/callback)
which deploy.sh resets on every deploy, so the dev server kept telling Box
"redirect me to localhost". Same thing would have hit prod.

Switched to request-based detection so the same code works on laptop, dev,
and prod:
- box_client.build_authorize_url and exchange_code_for_tokens now take
  redirect_uri as an explicit parameter (the two URIs MUST match — Box
  rejects the token exchange otherwise).
- New _box_redirect_uri() helper in api_server: prefers BOX_REDIRECT_URI
  if explicitly set (escape hatch), otherwise reads X-Forwarded-Host (set
  by Apache when behind the optical-dev / optical-prod reverse proxy,
  where the app is mounted at /ai_qc/), and falls back to request.host
  for direct local access.
- Dropped the per-env BOX_REDIRECT_URI from the four env files. Templates
  keep it commented out as documentation, and now also list all three
  redirect URIs you'll need to register in the Box developer console.
- box_client.is_configured() no longer gates on the redirect URI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:50:59 +02:00
nickviljoen
4939f990c5 Merge feature/box-oauth into develop (PR1: Box OAuth + token storage) 2026-04-27 15:42:46 +02:00
nickviljoen
c4e18fcd99 PR1: Box.com OAuth + token storage
First slice of the Box automation work. Adds the OAuth round-trip and a
smoke-test endpoint, but no automation logic or watcher yet — those land in
PR2 and PR3.

- New `backend/box_client.py`: OAuth helpers (build_authorize_url, exchange_code_for_tokens, refresh_tokens, revoke_tokens), JWT-signed state for CSRF protection, get_box_user, get_valid_access_token (refreshes if expired and persists the rotated refresh token Box returns on every refresh), and a list_folder_items helper used by the smoke test.
- New `backend/box_tokens.py`: thread-safe JSON-backed per-user token store at backend/box_tokens.json (gitignored — refresh tokens grant long-lived Box access). Persists access_token, refresh_token, computed access_token_expires_at, and the connected Box identity (id / login / name).
- New endpoints in `backend/api_server.py`:
  - `GET /auth/box/login` — auth-required, redirects the signed-in user to Box's authorize URL with a JWT-signed state.
  - `GET /auth/box/callback` — verifies the state, exchanges the code, fetches /users/me, persists the tokens, and returns a small self-closing HTML page (closes the popup if opened from one).
  - `GET /api/box/status` — auth-required, returns {connected, configured, box_user_login, …} for the current user.
  - `POST /api/box/disconnect` — auth-required, best-effort revoke at Box and clear the local tokens.
  - `GET /api/box/test_folder?folder_id=…` — auth-required smoke test that lists a Box folder using the user's stored tokens. Default folder_id is "0" (the user's All Files root). Used to prove the OAuth round-trip works end-to-end before PR3 wires the watcher.
- Box config in env (`BOX_CLIENT_ID` / `BOX_CLIENT_SECRET` / `BOX_REDIRECT_URI`) added to all four env files and both .env.template files (placeholders).

Box rotates refresh tokens — every successful refresh returns a new pair and invalidates the previous one. `get_valid_access_token()` always writes the new pair back via `box_tokens.save_tokens()`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 15:39:27 +02:00
nickviljoen
24a7db22ec Merge develop into main: CLAUDE.md doc refresh for v1.1.0 2026-04-27 14:44:22 +02:00
nickviljoen
d8062aaa07 Refresh CLAUDE.md docs for v1.1.0
- Bump headline client count from 8 to 10 (added AXA + Rank).
- Refresh the Client Configuration tables to match client_config.py (10 rows, video_general listed across all entries).
- Flip the Prod row in the Deployment Environments table from "Not yet stood up" to "Live (currently v1.1.0)" and drop the matching "prod-to-come" wording.
- Bump the Pre-Session Completion Checklist from "all 8 clients" to "all 10 clients".
- Add a "Self-service Client Access Requests" section under Recent System Enhancements covering the new client-picker tile, /api/access_request + /api/all_clients endpoints, the email_service module, and a "Settings Modal UX (Apr 2026)" section covering the simplified Reference Assets / Media Plan tabs and the context-aware modal footer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:43:30 +02:00
nickviljoen
8e973d0211 Merge develop into main for v1.1.0
Includes AXA + Rank clients, the settings-modal UX cleanup (single Name field
on Reference Assets and Media Plan tabs, context-aware modal footer) and the
new self-service Client Access Request flow with Mailgun-backed email.
2026-04-27 14:32:43 +02:00
nickviljoen
3d3caa1871 Merge feature/access-request-and-ux-cleanup into develop 2026-04-27 14:14:42 +02:00
nickviljoen
125c5e7064 Simplify settings UX and add client access request flow
Settings panel:
- Reference Assets tab: collapse the Brand Name + Tags + Description form to a single Name field; the user-entered name now drives the dropdown label on the main configuration page (falls back to filename for legacy records).
- Media Plan tab: add a Name field. Backend stores display_name on the plan record, and both the active-plan card and the main-page dropdown prefer display_name (falling back to original_filename for old plans).
- Modal footer is now context-aware: Save Profile + Cancel show only on the Profile / Create Profile tabs; Reference Assets / QC Tools / Media Plan show a single green Save button that closes the modal.

Client access request:
- New "Request Client Access" tile on the client picker, alongside the user's existing client tiles. Opens a modal that auto-fills name + email from the MSAL session (read-only), shows checkboxes for clients the user does not already have, and accepts an optional reason.
- New POST /api/access_request endpoint (auth-required) that takes identity from the verified session, validates the requested clients, looks up admin recipients via user_access.list_access_entries, and emails them via the new email_service module (Mailgun SMTP with STARTTLS). Reply-To is set to the requester. Logs an access_request event to the daily JSONL usage logs.
- New GET /api/all_clients endpoint so the form can list clients the requester currently cannot see.
- Mailgun SMTP credentials added to the four env files (and placeholders in the .env.template files) under SMTP_SERVER / SMTP_PORT / SMTP_USER / SMTP_PASSWORD / SENDER_EMAIL / ERROR_EMAIL / REPORT_EMAILS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:02:40 +02:00
nickviljoen
fdd591b371 Add AXA and Rank clients
Both get the static_general + video_general profile bundle, matching Honda's setup. Total clients goes from 8 to 10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:01:06 +02:00
nickviljoen
106f361a6d Revert prod-specific MSAL redirect slash strip (v1.0.1)
IT confirmed prod is registered with the trailing-slash form,
matching dev. The v1.0.1 hostname special-case is no longer needed —
both environments can preserve window.location.pathname as-is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 11:33:16 +02:00
nickviljoen
1e3adfa2b5 Strip trailing slash on MSAL redirect URI for optical-prod
Prod's Azure AD registration uses the no-trailing-slash form while
dev's uses the trailing-slash form. Rather than normalize via
helpdesk, we key the URI shape off the hostname so both environments
match their respective registrations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 22:40:04 +02:00
nickviljoen
43b1994967 Release v1.0.0 — first prod cut
Brings develop into main for the initial optical-prod.oliver.solutions
stand-up. Includes user access control, manual deploy scripts,
refreshed pricing + input/output token reporting, media-plan pivot-
cache fix, and the deploy.sh smoke-test widening.
2026-04-22 21:44:46 +02:00
nickviljoen
ffbec7e457 Load media-plan workbooks in read_only mode to skip pivot caches
openpyxl's default (read/write) loader deserializes pivot cache
records, which hangs for minutes on Amazon media plans that use pivot
tables. The GCP LB then cuts the request off with "upstream request
timeout" / "stream timeout".

read_only=True skips pivot cache parsing entirely, and our code only
uses iter_rows / sheetnames which are both supported in that mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 21:23:08 +02:00
nickviljoen
9771feaa3a Make deploy.sh smoke test retry for 30s instead of giving up at 3s
The service takes ~4s to come up on dev (75 QC modules + 14 profiles
import on start), just over the previous 3s sleep. This caused a
false-negative rollback. Now we poll /health every 2s for up to 30s
before declaring failure; same logic for the rollback-restart path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 21:09:10 +02:00
nickviljoen
f85ba2069f Refresh pricing and surface input/output tokens in reporting
- Update Gemini 2.5 Pro output pricing from $5 to $10 per 1M tokens
  (verified against ai.google.dev on 2026-04-22); OpenAI GPT-4o unchanged.
- Extend /api/client_usage_stats and /api/admin/users to return input
  tokens, output tokens, and per-provider cost breakdown.
- Surface the new data in the client Reporting tab and admin users
  table, with K/M token formatting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 20:59:41 +02:00
nickviljoen
d7d83cf90a Update CLAUDE.md with access control, environments, deploy scripts
- Bump stale counts to 14 profiles / 8 clients / 75 checks across
  overview, file structure, profile system, pre-session checklist.
- New section: User Access Control System (storage schema,
  user_access.py module surface, enforcement points, audit trail,
  frontend hooks).
- New section: Deployment Environments table (local/dev/prod/
  sandbox) with URLs, branches, servers, services, status.
- New section: Branch Strategy (develop→dev, main tags→prod,
  feature branches).
- New section: Deploy Scripts (deploy.sh, rollback.sh, health-check.sh).

Applied to both root CLAUDE.md and backend/CLAUDE.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:50:10 +02:00
nickviljoen
eaf68b1247 Update READMEs for user access control and new deploy flow
- Root README: add Environments section (dev live, prod pending),
  replace sandbox-era deploy block with deploy.sh usage, add user
  access to Capabilities.
- Backend README: rewrite Admin Panel section for user_access.json
  + User Access tab, add User Access Control feature, replace
  Production Deployment with deploy.sh/rollback.sh/health-check.sh,
  add the four admin/user_access API endpoints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:49:24 +02:00
nickviljoen
57f1848eb1 Add manual deploy/rollback/health scripts; remove stale rsync deploy
deploy.sh [dev|prod <tag>] handles git pull or tag checkout, reinstalls
deps only if requirements.txt changed, restarts the service, runs a
smoke test, and auto-rolls back on failure. rollback.sh reverts to the
checkpoint written by the last deploy (or to an explicit commit).
health-check.sh is a one-liner for "is the app alive?" checks.

Replaces the placeholder-config rsync-based deploy-to-prod.sh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:34:26 +02:00
nickviljoen
fee62d0766 Keep trailing slash on MSAL redirect URI for deployed envs
Azure AD has the trailing-slash form registered for the dev
environment. Previously we stripped it, causing AADSTS50011 on
first sign-in. Local dev (localhost) still uses no-path form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 18:23:31 +02:00
nickviljoen
c4a578500c Merge feature/user-access-control into develop 2026-04-22 12:33:30 +02:00
nickviljoen
6592c38b0a Add per-user client access control and admin management
Default-deny access model with admin grant/revoke via new User Access
tab. /api/clients filters by user grants; client-scoped endpoints
enforce access server-side. Admin role and client grants persist in
user_access.json with audit trail in usage logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 12:33:09 +02:00
nickviljoen
048d55dac3 Update README files to reflect current system state
8 clients, 14 profiles, 75 checks. Added documentation for video QC,
OCR measurements, media plans, PDF processing, admin panel, client
configuration, and all current API endpoints.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 15:02:42 +02:00
nickviljoen
20259dcad0 Add Honda client, video QC, session refresh, Amazon check tuning
- Add Honda client with static_general and video_general profiles
- Add video QC capability using Gemini native video analysis (4 checks:
  visual_quality, brand_consistency, text_legibility, pacing_flow)
- Add video_general profile assigned to all 8 clients
- Extend session lifetime with MSAL silent token refresh (proactive
  every 45min + reactive on expiry), switch cache to localStorage
- Re-enable OCR layout measurements for Amazon checks
- Add scope boundary notes to all 6 Amazon checks to prevent cross-
  check penalization (locale errors isolated to logo_country only)
- Relax margins left-alignment tolerance from 1% to 4% to account
  for logo lockup internal padding
- Update brand guidelines DB with Amazon localization matrix and
  processed Dove PDF summary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 14:53:52 +02:00
nickviljoen
ce13213b51 Tune all 6 WSJ check prompts based on campaign asset testing
Fixes based on testing 12 WSJ subscription campaign assets across 4
concepts and multiple formats (320x50 to 1080x1920):

- wsj_color_usage: Clarify Pop-on-Jewel is the primary approved
  combination, not a tier-mixing violation
- wsj_logo_compliance: Marketing assets use 30% longest side rule
  (not 60% standalone rule); fix stacked logotype sizing too
- wsj_capitalization_punctuation: Add explicit decision tree for
  Title Case vs Sentence Case — complete sentences use sentence case
- wsj_layout_composition: Add graphic/illustrative as valid design
  variation; add format awareness for small banners
- wsj_imagery_expression: Broaden neutral category to explicitly
  cover graphic/illustrative campaign assets
- wsj_typography_hierarchy: Add format awareness so small formats
  aren't penalised for fewer hierarchy levels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 15:33:13 +02:00
nickviljoen
0e211a3600 Fix client detection for Dow Jones, WSJ, MarketWatch and Boots profiles
Reports for WSJ/MarketWatch/Dow Jones profiles were falling through to
'general' because get_client_from_profile() and the inline fallback
mapping only handled loreal, diageo, unilever, and amazon. Added
mappings for dow_jones/dj_/marketwatch/mw_/wsj_ -> dow_jones and
boots_ -> boots in both locations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:20:09 +02:00
nickviljoen
402f2e6cf2 Reword batch completion popup to avoid implying QC pass/fail
Changed "Success: 2" to "Processed 2 of 2" so the popup clearly
reports processing status, not QC results. Processing errors only
shown when they occur.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:26:17 +02:00
nickviljoen
512e5ecb8b Strengthen hidden overlap detection with anti-autocomplete and proximity checks
LLM was autocompleting partial words and reading them as full text, missing
the hidden overlap. New approach: explicit "DO NOT AUTOCOMPLETE" instruction,
character-level boundary check (what background is each character on),
spatial proximity check (text touching product = fail regardless), and
concrete example using the actual test case ("para" where "p" is hidden
on dark purple product appears as "ara").

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:10:47 +02:00
nickviljoen
487b2b046b Add incomplete word detection to text_product_overlap check
Adds Step 4 (Hidden Overlap Detection) that catches text-product overlap
where text colour matches product colour, making overlapping letters
invisible. Instead of trying to see the hidden letter, the LLM detects
that a word is truncated/incomplete near the product edge, proving
overlap exists. E.g. "ragrância" instead of "Fragrância" where the
missing "F" is black on dark purple.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:03:29 +02:00
nickviljoen
6f3528b54f Add Boots client QC profile with 5 compliance checks and split CLAUDE.md client docs
New boots_static profile (5 checks, 2.0 weight each) for retail promotional
artwork compliance: caveat rules, brand name accuracy (~170 names), offer
mechanics, T&C wording, and currency/locale. Strict grading override (any
check <6 = Fail). Guidelines embedded from 7 thematic guidance documents.

Also splits client-specific documentation out of CLAUDE.md into separate
CLAUDE_LOREAL.md, CLAUDE_AMAZON.md, CLAUDE_BOOTS.md, and CLAUDE_DOW_JONES.md
files to reduce main file size.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 09:25:58 +02:00
nickviljoen
fd5661f452 Update docs with L'Oreal text_product_overlap check and tuning round 3 results
L'Oreal Static profile: 3 checks → 4 checks (added text_product_overlap).
Total QC checks: 65 → 66. Documents prompt tuning decisions, detection
accuracy across both test sets, and known gaps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:43:47 +02:00
nickviljoen
0e36998359 Check callout text for direct overlap, not just display text
Callout text was fully exempt from overlap checks, so the perfume
image text crammed against the product scored 10/10. Now callouts
are still checked for direct spatial overlap — being near the product
in clean space is OK (shampoo), but overlapping or touching the
product imagery is still a fail (perfume). Only the headline width
check (Step 4) remains display-text-only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:33:22 +02:00
nickviljoen
60a043fcb6 Tighten callout exemption: require visible connector line
Marketing copy near the product was being misclassified as exempt
callout text. Now callout exemption requires a visible connector/
pointer line drawn from the text to a product feature. Text without
a connector line is always classified as display text regardless
of size or proximity to the product.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:27:31 +02:00
nickviljoen
3b202fe1b1 Exempt callout/annotation text from text_product_overlap check
Small feature callouts with connector lines (e.g. "Plástico
reciclado" pointing to bottle cap) are standard cosmetics layout,
not an overlap problem. Shampoo image was false-positive failing
because callout text near the product was flagged.

Now classifies text as DISPLAY (headlines, titles, body — checked
strictly) vs CALLOUT (feature annotations with pointer lines —
exempt). Only display text triggers overlap failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:08:56 +02:00
nickviljoen
560ee8e85c Add headline width check to text_product_overlap prompt
LLM kept passing the mask image because headline is technically
above the densest part of the translucent shape. Added Step 4
(Headline Width Check) that catches wide single-line headlines
extending across the product's horizontal space — even if vertically
above. Includes exact bad/good examples matching the L'Oreal
mask vs shampoo assets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:02:00 +02:00
nickviljoen
a66fea7295 Fix text_product_overlap false positives: exclude callout lines and tighten hero zone
Shampoo image was incorrectly failing because:
1. Thin connector/callout lines (pointing from text to product features)
   were being treated as text overlap — now explicitly excluded
2. Hero zone was too wide — small scattered droplets/bubbles were
   extending the zone to cover the full image. Now clarified that
   hero zone is the compact cluster around the product only.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:53:55 +02:00
nickviljoen
66f1d1480b Add dedicated text_product_overlap check for L'Oreal profile
Revert text_readability to original (overlap is a layout issue, not a
readability one — LLM kept scoring it Pass because text was readable).

New text_product_overlap check uses a step-by-step approach:
1. Define the product hero zone (including translucent/glass elements)
2. Identify all marketing text
3. Check spatial overlap between text and hero zone
4. Compare good vs bad layout patterns

L'Oreal Static profile now has 4 checks at 2.5 weight each (was 3
checks at 3.33). Total check count: 66.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:46:13 +02:00
nickviljoen
bff2739604 Rewrite text-product overlap as step-by-step hero zone evaluation
LLM was still dismissing translucent 3D shapes as background. Rewrote
the check as a 3-step process (define hero zone, check overlap, score)
with explicit warning not to dismiss transparent elements. Added
concrete example matching the L'Oreal Absolut Repair Molecular mask
asset where the headline crosses the translucent sculpted shape.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 10:34:33 +02:00