Adds a new "Technical Details" card to generate_comprehensive_html_report()
between the summary and the per-check detailed results. Renders only
the fields present on the technical_report dict (file size, dimensions,
DPI, page count, duration, fonts, etc. — vary by file type) and shows
a prominent filename-vs-actual match badge when filename hints were
parsed.
If technical_report is absent or kind==unknown, the section is omitted
entirely so reports for assets we can't inspect (e.g. exotic
extensions) keep the existing layout unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Runs technical_check.inspect() immediately after file save on both
/api/start_analysis (visual flow) and /api/document/start_analysis
(document flow). The report is stashed on progress_tracker[session_id]
so it survives across the background thread boundary, then surfaces
two ways:
1. Each LLM check in the visual flow gets a "Technical metadata"
preamble prepended to its prompt via format_for_llm_prompt(), so the
model knows the file's actual dimensions, format, page count, etc.
without having to infer them visually.
2. result_data['technical_report'] in both flows carries the same dict
through to the frontend for UI rendering (next commit).
Pre-flight is best-effort: if it fails for any reason, analysis still
proceeds without the preamble (silent except for the report.errors
list).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New backend/technical_check.py extracts technical metadata from
uploaded assets via PIL (images), PyMuPDF (PDFs), and ffprobe (videos)
— no LLM, runs in milliseconds. Also opportunistically parses
dimension hints from the filename and compares them to the actual
file, returning a match/mismatch verdict.
Output is a JSON-serializable dict; format_for_llm_prompt() renders it
as a tight Markdown block that downstream prompts can prepend. Module
never raises — inspection errors land in `errors` so partial reports
still surface.
Standalone for this commit. Wiring into the upload flow and UI lands
in subsequent commits on this branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the intro count (9 → 12 clients), adds Google/HP/Ferrero to
the client name list, and adds three table rows for the new demo
clients (Doc column marked _scope pending_ until per-client docs land).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new clients in demo/eval phase. Each uses Honda-style minimal
setup (static_general + video_general only) until real scope and test
assets arrive. Descriptions are placeholders to be replaced once scope
is confirmed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the Dow Jones row from the client/profile table and the four
Dow Jones profile names from the pre-session profile-load checklist.
Also updates the intro paragraph counts (9 clients, 15 profiles, 60+
checks) and drops Dow Jones from the client name list, so the intro
no longer contradicts the table.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the 'dow_jones' block from CLIENT_PROFILES. After this, the
client picker no longer renders Dow Jones; the four archived profiles
are unreachable from user flows. Nine clients remain.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves the Dow Jones / MarketWatch / WSJ profile JSONs (4), check apps
(22), and CLAUDE_DOW_JONES.md into backend/_archive/dow_jones/. All
moves use git mv so history follows. Adds a restore-instructions
README. No loader changes needed — the archive lives outside the
scanned directories.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Step-by-step plan that turns the spec into 5 tasks: archive moves
(one commit), client_config edit (one commit), CLAUDE.md edits (one
commit), full verification, then push + PR with explicit user-confirm
gates. Defensive guards at each task halt execution if the codebase
has drifted from the spec.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the design for removing Dow Jones from Visual AI QC: archive
location (backend/_archive/dow_jones/), file moves, code edits, things
explicitly not touched, and verification commands. Implementation
follows in subsequent commits on this branch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updates the AXA client doc to reflect the 2026-05-10 state:
- Status line now reads 2026-05-10, covers Phase 6 (veraPDF), profile split,
and dev deploy
- New "AI usage across AXA tools" section for client-facing communication
(8 of 9 tools deterministic, only axa_pdf_diff uses AI)
- Open items expanded to include the pending source-PDF request and the
prod-deployment hold
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removed axa_pdf_accessibility from axa_policy_document (was 8 checks, now 7)
and created a new axa_accessibility profile that contains only that check.
Marked the new profile strict_grade: true so a single PDF/UA-1 rule failure
forces an unmistakable Fail badge on the report — mirrors how axes4 PAC is
used in practice (single-purpose, binary verdict).
Lets users run accessibility-only QC without sitting through the rest of
the policy-document checks, and removes weight from the policy-document
score that the accessibility check wasn't really earning (its 0/10 verdict
was dragging the overall grade in a way that obscured the content checks).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AXA's accessibility QC team uses axes4 PAC (PDF/UA-1 / Matterhorn Protocol)
as their compliance gate, but our existing 9-criterion deterministic check
runs surface-level only and would pass documents PAC fails. Wired up the
existing _run_verapdf() stub so veraPDF — the open-source Matterhorn
implementation — runs as a subprocess and drives the score when available.
Verified locally: veraPDF on EAA_v1.pdf reports the exact same Content (86)
and Metadata (1) failure counts as PAC's report on the same document family,
confirming protocol parity.
Falls back cleanly to the deterministic layer when veraPDF isn't installed,
so deploys are safe before the binary lands on dev/prod servers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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.
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>
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>
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>
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>
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.
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>
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>
- 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>
- 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>