gemini-embedding-2-preview collapses a plain contents=[...] string list into
one concatenated input (single vector), so naive batching silently drops
vectors. Add two safe, configurable levers to EmbeddingService.embed_texts():
- EMBEDDING_BATCH_SIZE (>1): attempts a real multi-content request, VERIFIES
the returned embedding count, and falls back to per-item embedding when the
API collapses or errors. One vector per input, in input order, always.
- EMBEDDING_MAX_CONCURRENCY (>1): bounded ThreadPoolExecutor across items or
chunks; results re-collected in input order.
Zero-vector degradation is preserved throughout: a failed input yields a zero
vector at its own position, never a dropped or misaligned entry. Defaults
(1/1) reproduce the previous strictly-sequential behavior exactly.
Batched ChromaDB querying:
- New query_similar_by_embeddings(): ONE collection.query() for N vectors,
results aligned to inputs; zero vectors are excluded (cosine vs zero vector
is meaningless) and come back empty; falls back to per-vector queries if the
batched call fails, keeping items independent.
- query_similar_by_embedding() now delegates to it, keeping the nested result
shape so existing callers are unaffected.
- moodboard _match_library_per_item() uses the batched query: N Chroma calls
per brief -> 1, and failed embeddings can no longer surface garbage matches.
Focused offline tests in backend/tests/test_embedding_optimization.py
(16 tests: ordering, collapse fallback, zero-vector slots, concurrency,
alignment, backward-compat shape, moodboard adapter).
Replace the sequential per-page loop (one Gemini vision call at a time with a
fixed 5s sleep) with a configurable semaphore pipeline in the new
services/moodboard_page_runner.py:
- MOODBOARD_PAGE_CONCURRENCY caps parallel page analyses (clamped [1,8]);
MOODBOARD_PAGE_DELAY paces page launches (0 disables; 5.0 keeps the
historical request rate); MOODBOARD_PAGE_RETRY_DELAYS sets the backoff
schedule; MOODBOARD_PAGE_ATTEMPT_TIMEOUT bounds each attempt.
- Results stay in page order regardless of completion order; progress
(current_page_status / processed / page_results) persists after every page.
- Retries now cover 429 + transient 5xx + transport errors (was 429-only),
honour a Retry-After hint, and give up immediately once the schedule is
exhausted (was: sleep after last attempt + one unprotected extra call) -
the per-page fallback then produces a neutral result as before.
- Image-batch retries reuse the same helper (same drop-on-failure contract).
Smoke checks: backend/scripts/smoke_moodboard_concurrency.py (stdlib-only,
pytest-collectable) - 11/11 pass.
The "Fabric direction" cards showed in-house garment matches for fabrics
SAPL doesn't stock. Calibrating against the live gemini-embedding-2 index
proved cosine similarity can't judge availability (a suede query scores
0.69 > cotton poplin 0.66; nonsense 0.51), so availability must be a DB
fact.
- _fabric_status: vocabulary-driven, matches STRUCTURED fabric fields only
(composition/weave/texture/pattern), never free-text description — kills
the blend/soft/coloured false-positives; replaces loose check_fabric_library
in the moodboard grounding.
- _match_library_per_item: gap-gate — a sourcing-gap fabric surfaces zero
in-house garments.
- embed_texts: gemini-embedding-2 returns ONE embedding for contents=[N],
silently collapsing N per-item queries to 1; embed per-text instead
(restores silhouette/print matching that was dropped by a zip).
- _enrich_matches: calibrated similarity floor (moodboard_match_min_similarity
0.58, env MOODBOARD_MATCH_MIN_SIM) drops nonsense-tier hits.
- Frontend: subtle library-status hint on fabric cards (in / similar / not in
library) so an empty gap card doesn't read as broken.
Validated live vs real DB: tweed/suede/terry→gap (no in-house), cotton
poplin/twill→have, satin/organza+viscose→similar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The UAT tenant seeds Deji Das as saplind-Das, but prod's template master (portal2)
carries saplind-Deji (live-verified 2026-07-14). Remap owner usernames at the push
boundary, active only for a UAT integration host; prod keeps the original usernames.
Unblocks the in-app push (all 9 Category templates now POST clean to UAT).
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>
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>
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>
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>
New "Studio" tab (/concept): drop a garment shape + a fabric swatch, describe
the item, and generate a concept preview. Proxies SAPL's commissioned
AIBrandSolutions Swatch→Garment backend server-side (no vendor creds needed),
persists the result to data/concepts/, and serves it same-origin behind our
login/RBAC.
- backend/routers/concept.py: POST /api/concept/swatch-to-garment (moodboard:edit)
- config: swatch_to_garment_base
- main.py: mount /concepts (fresh-checkout-safe mkdir) + register router
- frontend: ConceptStudio page, "Studio" nav tab, swatchToGarment()/conceptImageUrl()
- docs: 24-Jun design-library meeting notes + STATUS entry
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a hide_demo_orders flag (default on) and route the order-list endpoints
through a single _orders_q() helper that excludes "*-Demo" styles, so the
TNA-8/10/11-Demo orders from Stitch don't reach pilot users. Reversible via
config; detail lookups by style_number are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add "Continue with Google" to the login page, gated to the company domain
(saplind.com) and invite-first (the user must already exist). Verifies the
Google ID token server-side and issues our existing app token — RBAC unchanged.
Dormant until IT provides GOOGLE_CLIENT_ID: GET /api/auth/config reports
whether SSO is enabled so the button only renders when configured; password
login is unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>