- .env.uat.example: template showing UAT-specific env vars
- .env.uat added to .gitignore (contains secrets)
- Real .env.uat lives at /etc/dokploy/compose/sapl-sapl-uat/code/
- docker-compose.uat.yml: separate UAT stack with sapl_uat_data volume
and uat.sapl.ai-automation-hub.tech Traefik routing
- deploy-uat.yml: auto-deploys UAT stack on push to uat branch
Production stack (sapl-sapl-am5vak) untouched — existing ci.yml
continues to deploy main to production as before.
Buildable-now foundation for auto-completing TNA milestones (no live-integration
dependency), per docs/plans/tna-milestone-autocomplete.md and Pratik's 3 Jul data
reality:
- services/tna/milestone_map.py — classify each milestone by source (WFX order/PO/
GRN · Stitch production · SAPL approval · manual) + coverage (AUTO_NOW/NEEDS_QR/
MANUAL).
- services/tna/reconcile.py — idempotent, monotonic completion + delay + order-risk
(never un-ticks a done milestone; measures delay vs the frozen plan).
- Phase 2b: models/tna_approval.py (append-only approval log) + services/tna/
approvals.py; decide_recommendation now records each planner decision, feeding the
Approvals milestone auto-tick with zero ERP dependency.
- scripts/smoke_test_tna_phase0.py — 24/24 passing.
Not wired into any live sync yet; ready for Phase 1 when Stitch's TNA Task API lands.
NOT deployed (prod deploys are on hold after the 7 Jul incident).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nginx cached the backend container's IP at startup (proxy_pass http://backend),
so every backend redeploy (new container IP) left it dialing the old dead IP →
"upstream timed out while connecting" → 504 on all /api calls until the frontend
was manually restarted.
Add Docker's resolver (127.0.0.11) + a variable proxy_pass so nginx re-resolves
the backend every ~10s. Also fail fast on a truly-down backend (connect 10s) while
allowing long AI calls (read 300s). Makes the stack self-heal across deploys.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The compose environment override didn't reliably reach the backend container on
Dokploy (env-only redeploy didn't rebuild/recreate). Setting the PUBLIC OAuth
client id as the config.py default guarantees /auth/config returns it — the code
change forces a backend rebuild, and the value is compiled in. Env still overrides.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Sign in with Google" button renders only when /auth/config returns a
google_client_id. Prod's Dokploy env never had GOOGLE_CLIENT_ID set, so the
button was hidden on sapl.ai-automation-hub.tech (visible locally via .env).
Add it to the compose environment block. The client ID is a public identifier
(already exposed to the browser), so it's safe to commit; the client secret is
not needed by the ID-token verification flow and stays out of the repo.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopt Google's Open Knowledge Format (curated markdown + YAML frontmatter)
as KAARIGAR's domain-knowledge layer — not a RAG replacement (KAARIGAR is a
tool-calling agent), but the stable, curated knowledge it needs every turn to
reason correctly about SAPL.
- New OKF bundle backend/knowledge/ (glossary, wfx-oc + tna-milestones schemas,
tna-approval rule, stitch-ids join, when-to-use tool guide), interlinked.
- Loader services/hermes/knowledge.py: frontmatter-stripped, lru_cache, fail-soft.
- agent.py injects it into system_instruction (~2.5K tokens/turn).
- Tested live (real Gemini, local DB): tool routing + delay/owner + Library
facet behavior all match the curated rules.
Also: STATUS.md updated; TNA milestone auto-complete plan added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the in-app assistant to KAARIGAR (the master artisan) and give it a
visual identity — the golden dress-form mark — across the app.
- System prompt identity: "You are KAARIGAR…"
- Ask command bar: button, input, thinking + error copy
- Top-nav: mark beside the SAPL wordmark
- Login: mark beside the "Sign in" heading (desktop + mobile)
- Favicon: browser tab + iOS icon (replaces the placeholder bolt)
- Enlarge the sign-out door icon for visibility
Internal identifiers (route /api/hermes, filenames, types) unchanged — no
API path change, so this is a UI-only, zero-risk deploy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
"show me beige dresses" now applies the real Refine facets (chips +
filtered grid + count) instead of listing one item and pulsing a card.
- Library.tsx reads URL facet params (?colour=&category=&fabric=&dna=
&season=&q=) -> native facetSel selections (substring match, _->space)
- search_library emits a "Show in Library" action with those facets; one
call both answers and filters the UI. Colour matching made lenient to
match the Library's Colour facet (counts agree: beige=8).
- prompt: browse intents -> filter UI (one-line reply, no long lists,
single call); factual -> text; TNA "take me to" -> navigate+highlight.
Verified: beige dresses -> /?colour=beige&category=dress; beige pieces
-> 8; "how many late" -> text only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
search_library was semantic-only, so 'black dresses' returned fuzzy,
polluted results (8 sources for 3 real matches). Now the model extracts
attributes into structured params (category/colour/fabric_family/
design_dna/season) matched exactly against tags; colour is role-aware
(dominant Hero colour). Citations == matched set.
Verified: black dresses -> 3 (all black-dominant); navy blouses -> 2;
knit tops -> 1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HERMES no longer just answers; it can drive the app. New `navigate` tool
lets the agent take the user to a page and highlight a specific item; the
backend returns actions[] with focus-aware paths (/tna?focus=order:...).
A global HermesFocusRunner scrolls to + pulses the matching
[data-hermes-focus] card once the page loads. AskHermes auto-runs the
action (cancellable). Tagged TNA order cards + Library garment cards.
Verified: "take me to the late Primark order" -> query_orders+navigate
-> /tna?focus=order:8863-1264674; broad info queries don't navigate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 of the master-brain unifier (scope-hermes-for-pratik.md §9-12).
An LLM agent whose tools are our own backend functions: the user asks in
natural language, the agent picks tool(s), they run server-side AS the
logged-in user (RBAC applies), and it answers over the real results with
clickable citations. Reuses the Market Scraper's Gemini function-calling
loop, generalised and made synchronous.
- services/hermes/tools.py: search_library + query_orders (portable,
{summary, rows, citations})
- services/hermes/agent.py: ask_hermes sync loop (never raises)
- routers/hermes.py: POST /api/hermes/ask (auth as current user)
- frontend AskHermes.tsx: floating Ask bar (⌘J) + grounded answer +
citation cards; mounted globally
Tools verified against real data locally; live Gemini answer verified on
prod (local key is 403 project-denied, same as Scraper/Concept Studio).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
History in Trends and Moodboard (and Boards) leaked across users
because those sections were scoped by department, not by user.
Rework them to per-user ownership:
- rbac: add can_see_all(), owner_scope_query(), can_access_owned()
- add created_by to TrendSearch + MoodboardDecode (boot-time self-heal);
Boards reuse existing owner_id
- scraper/moodboard/collections: stamp owner on create, scope every
list, and 404-guard every detail-GET + mutation by owner
- by-garment saved-state only reflects the caller's own boards
Library + Fabric stay public; TNA/Inbox/Review/Analytics stay
team-shared; Admins and 'all'-scope roles still see everyone's.
Verified: scripts/smoke_test_user_isolation.py 14/14.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each role's detail panel gains an "Add someone to <role>" box: type an
email and they're placed in that role. Existing users are switched to the
role; new company SSO (@saplind.com) emails are auto-created (no password,
Google sign-in). Non-SSO new emails are directed to the Users tab. Backend
POST /admin/roles/{role}/members upserts with the last-active-admin guard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Creating a user whose email is on the SSO domain (@saplind.com) no longer
requires an initial password — they sign in with Google. Backend
auto-generates an unusable password for such accounts; non-SSO emails
still require one. Admin create form now disables the password field for
SSO emails, shows an "ⓘ" hint explaining it, and relaxes validation
accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First-time @saplind.com sign-in via Google now creates the user
automatically with google_default_role (default "Viewer" — read-only on
the design surfaces), instead of rejecting unknown emails. Domain +
email_verified are still enforced; existing users are never modified, so
anyone already assigned a higher role (e.g. Admin) keeps it. Admins raise
access afterwards in /admin. Set google_default_role="" to revert to
invite-only. Also stores google_client_secret in config (unused by the
ID-token flow) for completeness.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the design-analytics page (all real data, no mock):
- /api/analytics now accepts filter params (fabric_family, design_dna,
design_team_city, season, segment, status, category, collection) and
scopes every distribution, colour palette, growth series and review
health to the filtered garment set; returns filter_options + growth.
- New tag breakdowns surfaced (neckline, sleeve, length, hem, closure,
fabric_texture, embellishment, weave, group) plus design-team-city.
- Library-growth trend chart from garment created_at (added + cumulative)
— the team-adoption curve.
- Frontend filter bar (only shows dimensions with >1 value), recharts
growth chart, new sections, and mobile-friendly labels/padding.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The overlay lived inside the TopNav <header>, whose backdrop-filter makes it
the containing block for position:fixed — so "fixed inset-0" was relative to
the 64px header: pinned to the top on desktop and clipped (invisible) on
mobile. Render it through createPortal(document.body) so it covers the real
viewport and centres.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 22px top-bar Exit animation was invisible — the click navigated away
before it could be seen. Now clicking it raises a full-screen centred
overlay that plays the Exit animation large (w-44 → sm:w-56) with a
"Signing out… / See you soon" caption, and only clears auth + routes to
/login on the animation's `complete` event (2.2s safety-net fallback).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an Exit (dotLottie) sign-out button beside Search in the top bar,
visible on every screen size (easy to find, works on touch). It rests on
the first frame, replays on hover, and on click plays through before
clearing auth and routing to /login.
The asset was only in dist/ (build output, wiped on rebuild) — moved it to
src/assets/Exit.lottie so it's tracked + bundled (emits as a hashed asset;
vite already keeps .lottie out of the inline path).
tsc -b + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- BottomNav: 4 quick tabs + a "More" tab opening a sheet with every
remaining section plus "+ New garment" and account/sign-out — so all
pages (not just the first 5) are reachable from the bottom bar.
- TopNav: remove the mobile hamburger + slide-down drawer (and now-unused
icons/state); the bottom tab bar fully owns mobile navigation. Desktop
top bar is unchanged.
tsc -b + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- BottomNav: mobile-only fixed bottom tab bar with the top ~5 sections
(thumb-friendly), respecting the iOS safe-area inset; the full list +
account stay in the TopNav hamburger drawer. SideRail remains lg-only.
- Shell: bottom padding on mobile so content clears the bar.
- LottieLoader: accept responsive width/height via className (not just a
fixed px size); upload/trends/studio animations now scale down on phones
(w-28 → sm:w-40ish) and their modals tighten padding on small screens.
Pages (Fabrics/Studio/Trends) already stacked responsively; this fills the
navigation + animation-sizing gaps. tsc -b + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The running agent's animation/progress was a small inline panel in the
left form card. Lift it into a centred pop-up modal (like the studio
loaders): large Searching animation, current action, elapsed + items, and
the live thinking-steps log. Since research runs a minute or two, the modal
is dismissible (Hide / backdrop) and the form card keeps a compact
"Researching…" indicator with a "Show live progress" button to reopen it.
tsc -b + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On every restart the frontend sat empty until a flood of outbound GETs
finished. Cause: the lifespan AWAITED the initial Stitch pull (50+ pages of
style-plan-master) — plus the TNA-template and WFX syncs — BEFORE yielding.
Uvicorn doesn't accept requests until the lifespan reaches `yield`, so the
API was unavailable for the whole initial-sync duration.
Move all three off the pre-yield path:
- Stitch initial sync + 2-min loop → background task (loop already sleeps
first, so no double sync).
- TNA template master sync → its own background task.
- WFX mirror → background loop via asyncio.to_thread (the sync is blocking
urllib; running it on the loop also stalled every request each cycle).
First sync now fires immediately instead of after a 30s wait.
Startup now reaches `yield` at once; data fills in as syncs land.
py_compile clean; SQLite engine already check_same_thread=False.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A fabric upload 502'd on a TimeoutError: the genai call hung past the 90s
client timeout and raised TimeoutError — but the retry loop only caught
ServerError (503), so the timeout bypassed the backoff entirely.
- fabric extraction: retry a transient tuple (ServerError, TimeoutError,
aiohttp.ClientError) with the existing exponential backoff; lower the
per-request timeout 90s → 60s so a hung socket fails fast and is retried
instead of stalling the batch.
- same broadening applied to the concept + product-studio enhance retries,
which had the identical 503-only catch.
py_compile + imports clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The corner Lottie was tiny and easy to miss. Replace it with a centred
pop-up modal during bulk upload: large Upload animation, "Reading X of Y…",
the added/duplicate/failed tally, a progress bar, and the keep-tab-open hint.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The garment Upload page got the Upload animation but the Fabric Library's
"Upload swatch cards" flow still showed only a text progress line. Add the
UploadLottie beside the live progress while a batch is reading.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the static border-spinners with dotLottie animations:
- Upload (garment ingest "Analysing/Uploading…") → Upload_file_icon
- Trends/Scraper (Starting + live agent header) → Searching
- Concept Studio + Product Studio (generating) → Digital_Designer
- add @lottiefiles/dotlottie-react; shared components/LottieLoader.tsx
exposes UploadLottie / SearchingLottie / DesignerLottie (loop+autoplay,
sized box), loading the .lottie assets via ?url imports.
- vite: assetsInlineLimit returns false for .lottie so small ones aren't
inlined as base64 (the player needs a real URL to fetch the binary).
- drop the now-unused Scraper Spinner helper.
tsc -b + build clean; all three .lottie emitted as hashed assets.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Uploading the same card twice created two rows — there was no duplicate
check. design_no can't be the key: the AI label-read varies between runs
(saw "SAPL-LOT NO-8658" + brand RAICHALLO vs "...8658 RAICHALLO" + no brand
for the same card). The reliable identity is the image bytes.
- new Fabric.source_hash (sha256 of the source card image), indexed;
ensure_fabric_schema() ALTERs it onto the existing prod table at startup,
and backfill_source_hashes() hashes the already-seeded rows so they dedupe
too. Wired into the lifespan.
- POST /fabrics/upload hashes the bytes BEFORE extraction: a known hash
returns the existing fabric with {duplicate:true} + HTTP 200 (and skips the
wasted Gemini call); new cards store the hash and return {duplicate:false}.
- frontend: bulk upload counts duplicates as "skipped" (not re-added, not
failed), shown live ("· N dup") and in the summary.
py_compile + tsc -b + build clean; schema self-heal + dedupe lookup unit-tested.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bulk swatch upload was failing 5 of 7 cards: the extractor leaned on the
genai SDK's built-in retry, which gives up too fast under a sustained 503
"high demand" surge, so the error reached the route → 502 per card.
- backend: FabricExtractionService.extract() now retries ServerError (5xx)
with exponential backoff + jitter (~2/4/8/16s, ~36s worst case) before
giving up. Benefits every caller (bulk upload, single upload, the seeder).
- frontend: a failed card is collected (not just counted); a "↻ Retry N
failed" button re-runs only the failures with one tap — no re-selecting,
so a batch self-heals once the spike passes.
tsc -b + build + py_compile clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Upload a swatch card" only took one image at a time. Now:
- the primary button opens a multi-file picker (select many cards), and
- a secondary "or upload a whole folder →" link opens a folder picker
(webkitdirectory, set imperatively since it isn't a typed React prop).
Selected images are filtered (jpg/png/webp) and read one at a time — the
Gemini extractor throttles under load, so sequential with live progress
("Reading 3/40…", running added/failed tally) is safest. A card that fails
doesn't abort the batch; it's counted and reported so the rest still import.
Single-card uploads still open the detail modal as before.
tsc -b + build clean. Frontend-only — reuses POST /api/fabrics/upload per card.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stitch committed (Manish Arora, 2026-06-29) to release a TNA API set on/before
2026-07-07: GET TNA Task / TNA Task Category / TNA Assign Users, and POST TNA
Template Creation/Update (the write path we previously lacked).
Add a thin client for those four endpoints, behind the new
stitch_tna_api_enabled flag (OFF by default — no runtime change until live):
- reuses the existing STITCH-KEY integration auth + stitch_api_host
- endpoint paths are config-overridable (exact paths unpublished) so the real
ones drop in from Stitch's spec without a code change
- _extract_rows tolerates both list and paginated (data.results+next) envelopes
- fetch_tna_tasks / fetch_tna_task_categories / fetch_tna_assign_users (GET),
upsert_tna_template (POST), tna_api_status(), and a read-only probe()
- guards raise StitchTnaApiError when disabled or unconfigured
Compiles/imports; flag-off guard + envelope parser unit-checked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prod "Could not enhance the prompt" had two causes:
1. Truncation: max_output_tokens=600 with thinking_budget=1024 left no
room for the JSON after the model's thinking, so the response was cut
off mid-string → JSONDecodeError → 502. Worse in Concept Studio,
which sends two images and reasons more.
2. Transient Gemini 503 ("high demand") collapsed into the same 502.
Fix (both /concept and /product-studio enhance):
- max_output_tokens 600 → 2048, thinking_budget 1024 → 512.
- response_schema forces a closed {enhanced_prompt} object (no free text).
- retry the call 3x with backoff on ServerError; surface a clear 503
("busy, try again") instead of a generic 502.
- log the raw model output on parse failure for diagnosis.
Verified end-to-end against real Gemini (local key): retries past a live
503 and returns a brief that reads both the shape and the fabric.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bring Product Studio's prompt enhancer to Concept Studio, but for this
tool it reads BOTH inputs: the garment shape and the fabric swatch,
plus the user's draft and the swatch dimensions.
- backend POST /api/concept/enhance-prompt (gated moodboard:edit):
resolves the garment + swatch (upload OR library garment_id /
swatch_fabric_id / swatch_garment_id) off our own store, then runs
Gemini vision with both images + a fashion-design-director system
prompt → one precise "describe your item" brief. 503 without a key.
- frontend: conceptEnhance() + an ✦ Enhance prompt button by the
"Describe your item" field, enabled once both a shape and a fabric
are chosen; replaces the draft with the enhanced brief. Generate and
Enhance now share one appendInputs() helper.
tsc -b + build + py_compile clean; router exposes both routes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picking a fabric in the studio did nothing locally: the handler fetched
the swatch image (imageUrl → http://localhost:8000/...) into a File to
re-upload it, but that fetch is cross-origin from the Vite dev server and
CORS-blocked, so it threw and no swatch was set. In prod ASSET_ORIGIN is
"" (same-origin) so it happened to work — hence "only breaks locally".
Reference the fabric by id instead of re-fetching its bytes:
- backend: swatch_to_garment accepts swatch_fabric_id and loads the
swatch (or card) image off the local store, like swatch_garment_id.
- frontend: a picked fabric is a {kind:'fabric', fabricId} pick that
posts swatch_fabric_id; the "Use in Studio" deep-link passes fabricId
too, so it stops re-fetching as well.
Backend-independent of origin and one fewer image round-trip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"Pick from library" for the Fabric swatch slot was listing garments
(it always called listGarments), so users picked a whole garment as
their "fabric". Make LibraryPicker mode-aware: the garment slot reads
the Garment library; the swatch slot reads the Fabric library
(/api/fabrics) and shows swatch images + D.NO/blend. On select, the
chosen swatch image is fetched into a File so it posts through the
existing swatch upload path — no backend change needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Production runs a separate SQLite file on a persistent volume that was
never seeded, so /fabrics showed 0 swatches even though 59 were ingested
locally. Ship a committed snapshot (seed/fabrics_seed.json + 118 card/
swatch images) and an idempotent startup seeder:
- inserts the 59 rows only when the fabrics table is empty (never
overwrites edits / human-verified data on a populated prod DB)
- copies swatch images into the images volume only when missing
- coerces ISO datetime strings back to datetime for SQLite
Wired into the lifespan startup right after RBAC seeding.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GarmentDetailModal passed `|| null` for collection_name,
fabric_composition and description, but updateGarment expects
`string | undefined`. The tsc -b error failed CI's lint-and-test
job, which gates the deploy job (needs: [lint-and-test]) — so
the Jun 29 fabric-library build never deployed. Use `|| undefined`
to match the style_number field above.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Append a 2026-06-26 section to the HERMES scope note: promote HERMES from the TNA
WhatsApp coach to the atelier's brain across the whole app (Library/Studio/Trends/
Inbox/TNA). One persona, two surfaces (proactive push + an "Ask HERMES" bar); the
TNA coaching becomes its flagship skill. Notes the agent-with-tools design (reuses
our Gemini function-calling engine), grounding + RBAC, carried-over privacy
guardrails, and a 3-phase rollout (in-app Ask + morning brief first; person-level
WhatsApp + scorecards last, HR-gated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The demo link's fillDemo helper hard-coded real demo credentials into the frontend
bundle, so anyone with the login URL or viewing source could sign in. Remove the
link, the fillDemo helper, and its CSS. Verified the creds are no longer present in
the built bundle.
Follow-up: the demo account still exists server-side (and the creds are now in git
history) — deactivate it or rotate its password via /admin for full closure.
tsc -b + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same fit-to-page treatment: h-screen flex layout, compact custom header (drop
PageHeader), height-cap the two input slots (aspect-[3/4] -> h-36), tighten the
description/dimensions/aspect controls with Generate pinned at the bottom, and let
the result panel fill the column height. The old two aspect-[3/4] boxes pushed the
controls below the fold.
Fit smoke 3/3 PASS (scrollHeight==innerHeight at 900px, Generate visible, both
slots present). tsc -b + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Our-side value-add on the "Describe the shoot" field: ✦ Enhance prompt reads the
uploaded garment image + the user's draft and rewrites it into a precise shoot
direction tuned to how Product Studio Pro is directed (subject/model, pose & angle,
background, lighting, framing, and always "keep the garment's exact colour/pattern/
texture/details sharp").
- backend POST /api/product-studio/enhance-prompt (gated moodboard:edit): Gemini
vision (gemini-3-flash-preview, JSON out) with a product-photography art-director
system prompt; designs a default e-commerce shoot if the draft is empty; 503 if
no Gemini key.
- frontend: client.ts productStudioEnhance() + an Enhance button by the textarea
(disabled until a garment is uploaded; replaces the draft with the enhanced prompt).
Frontend-flow smoke 3/3 PASS (disabled -> enabled-on-upload -> textarea filled).
tsc -b + build + py_compile clean. Real Gemini call verified on prod (key lives there).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Output was hard-coded output_resolution:"HD" (896x1152), so texture looked soft
vs Swatch->Garment. The tool also supports 2K/4K — probed 4K = 3712x4608 (~16x
the pixels). Add a Quality selector (HD/2K/4K) defaulting to 4K; bump the backend
default HD->2K; raise the poll ceiling 120->180s and client timeout 180->300s for
the heavier 4K render. Layout still fits the page (4/4 fit smoke).
tsc -b + build + py_compile clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first native cut used two oversized aspect-[3/4] boxes (~700px tall each),
pushing the prompt/aspect/generate controls below the fold. Rework to fit-to-page:
h-screen flex layout, a compact height-capped upload preview (object-contain), a
left control column with Generate pinned at the bottom, and a result panel that
fills the column height. Drop the now-unused PageHeader import.
Fit smoke: scrollHeight==innerHeight at 900px viewport, Generate visible without
scroll. tsc -b + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the earlier iframe embed: per Ajay, it should be our SAPL frontend over
the vendor's backend, exactly like Concept Studio — not the vendor's branded page.
Live-probed the backend and confirmed the contract (and corrected the trial-quota
worry): /api/tenant-info shows isTrial:false (paid tenant, no quota); /api/edit-image
works anonymously with the image as a base64 data-URL -> requestId; poll
/api/edit-status until COMPLETED -> images[].
- backend/routers/product_studio.py: POST /api/product-studio/generate
(gated moodboard:edit) base64s the upload, submits, polls to completion,
downloads the result, persists to data/concepts (served /concepts/).
config.product_studio_base; registered in main.py.
- frontend/pages/ProductStudio.tsx: native SAPL page (upload, prompt + seed chips,
aspect ratio, generate, result, download). client.ts productStudioGenerate().
Smokes: end-to-end backend via the real vendor with a synthetic image 4/4 PASS
(851 KB PNG persisted); native-UI render 5/5 PASS (no iframe). tsc -b + build +
py_compile clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AIBrandSolutions' Product Studio Pro (flat garment + prompt -> on-model shots)
is a full async chat/canvas app with a trial session + per-browser-id quota —
not the no-auth one-shot endpoint Swatch->Garment was. A server-side proxy would
share one trial quota across all users; embedding gives each user their own
browser session/quota, needs no creds (works anonymously from the link), and the
tool sets no frame-bust headers.
- New "Product Studio" nav tab -> /product-studio (pages/ProductStudio.tsx),
gated on the moodboard module like Concept Studio: full-height iframe to the
tool + an "Open in new tab" fallback.
- docs/plans/product-studio-integration.md: reverse-mapped async contract
(edit-image -> edit-status, X-Browser-ID trial, R2 storage) for the Phase-2
native persist-to-library proxy (deferred until SAPL has a vendor tenant key).
Embed smoke 5/5 PASS (gated route, iframe->tool, nav tab, fallback link,
0 errors). tsc -b + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>