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.
actions/setup-python@v5 cannot provision on Debian containers — the
python-versions manifest only ships Ubuntu/macOS/Windows builds, so the
job failed with "The version '3.11' ... was not found for this operating
system". The static-checks job runs on the runner default image
(node:20-bullseye); install python3 + pip via apt (proven path) and use
python3 / python3 -m pip (Debian has no bare 'python' binary).
- compose-validate: Debian bullseye's docker.io ships no compose plugin
(docker: 'compose' is not a docker command, exit 125). Run compose
config -q + resolved-config secret guards inside the compose-capable
docker:28-cli helper (same image family as deploy-uat.yml), feeding the
compose file via stdin and PH_* placeholder env via --env-file.
- static-checks: python:3.11-slim has no Node so actions/checkout failed
(exec: node not found, exit 127). Drop the job-level container (job runs
on the runner default node:20-bullseye image) and provision Python 3.11
via actions/setup-python@v5.
- deploy-uat: add workflow_dispatch trigger and let the deploy job run on
manual dispatch against uat (previously always skipped, e.g. run #159);
push to uat unchanged, PRs to uat still lint-only.
Pratik: the "From the deck" strips showed the same ~8 pages under every
category. The filter included any page that merely mentioned a category, and
most deck pages are full looks (colour+fabric+silhouette) → qualified for all.
Replace with assignDeckPages: each page goes to a single category — its
page_type enum (colour_direction/fabric/print/silhouette) wins, else its
dominant content array. No page repeats across sections; each strip is
genuinely relevant. Verified on a real 14-page decode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pratik feedback — the decode read as a wall of text. The deck pages were
already rendered + tagged during analysis but never displayed. Add a
"From the deck" thumbnail strip (deckPagesFor + DeckStrip) to the Colour /
Fabric / Print / Silhouette sections, sourced from page_results by category
(ranked by relevance, click → lightbox). Zero extra AI cost.
Also render the Fabric/Print/Silhouette sections even when no AI swatches
were generated (previously hidden) — deck strip + brief text — so a
text-only brief still surfaces the buyer's imagery.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main was 17 commits behind + diverged from uat; promoted via cherry-pick of
the 7 session commits onto main (261b0e6), Dokploy deploy verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Concepts tab shipped empty — but Concept/Product Studio outputs
(data/concepts) and moodboard concept renders already exist from before the
feature. Add an idempotent POST /api/garments/concepts/backfill that imports
them as origin=ai_concept entries (deterministic dest filename → re-runs are
no-ops; moodboard renders paired to their hero-piece names), plus an "Import
existing" button on the Concepts tab that runs it and refreshes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Concept/Product Studio + moodboard outputs were one-shot generators whose
images never entered the library. Persist them as first-class, origin-tagged
entries so the public library stays clean.
- Garment.origin (made|ai_concept) + origin_source; rbac self-heal
(existing rows default to 'made').
- filter_search excludes ai_concept by default (concepts never leak into the
main library, studio pickers, similar, or boards); origin=ai_concept|all
param threaded through /garments + /search/filter; origin in serializer.
- POST /api/garments/concept: copies a generator image into the library store
and creates an origin=ai_concept garment (no style number / QR / production).
- Frontend: Library Made/Concepts tab toggle; amber CONCEPT badge on cards;
concept cards titled by description; saveConcept client fn; "Save to Library"
on Concept Studio + Product Studio results + moodboard starter-kit cards.
Validated live: concept created → excluded from Made grid, shown in Concepts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Technical flat sketches generated unconditionally and the Design Starter
Kits section was assembled regardless of what the designer chose — so
picking only "print & fabric swatches" still produced garment flats + a
full starter-kit block they never asked for (wasted image spend on a
garment design they won't use).
Starter kits are a garment-design deliverable, so tie them to the garment
opt-in: wants_garments = generate_visuals AND "concepts" in visual_types
now gates construction-spec drafting, flat-sketch generation, and the
starter-kit assembly. Swatches-only runs skip all of it (frontend already
hides an empty section); "garment concepts"/"everything" get the full kit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Replace the static pulsing-dots in PdfDecodeProgress with the "cat Mark
loading" dotLottie. Move the file into src/assets (no space) so it goes
through the vite ?url bundle + cache-busting like the other loaders, and
add a DecodingLottie preset to LottieLoader.
Docs: archive the granular 2026-07-25 scraper iteration log to
STATUS-ARCHIVE.md and slim STATUS.md to a summary; mark the moodboard
interactive-flow work as shipped (da212d0, live on UAT).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A custom role created in the admin UI with a name matching a DEFAULT_ROLES entry
(e.g. a hand-made 'Designer') shadowed the department model, because the seed only
synced is_system roles. Now any DEFAULT_ROLES-named role is synced to the spec and
marked system; custom roles with other names are untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dehx6goqDV2TiQ92dFFWGP
Notifications (models/notification.py, services/notification_service.py,
routers/notifications.py):
- The bell — a notification lands for the content owner (+ thread participants)
when someone likes/comments on their post or a board/brief/trend they shared.
Self-actions don't notify; ownerless system events (ranges/orders) don't spam.
- Announcements (admin/management-gated) fan out to every active user and post
to the feed with a 📢.
- Frontend: NotificationBell in TopNav (unread badge + dropdown) + an Announce
toggle in the feed composer. Verified smoke_test_notifications 8/8.
Department-scoped RBAC (services/rbac.py):
- One role per department managing only its own features on top of the common
surfaces (library/boards/ingest): Admin (global) · Designer · Merchant ·
Planner · Auditor · Leadership · Viewer. Users/Data stay Admin-only.
- Seed keeps code-owned system roles in sync and retires removed ones (0-user
guard). Personas map to the roles (feed_digest._persona).
- A Designer sees only the design + common nav; shared boards open for all
(common) but shared briefs/trends stay department-gated.
Admin consolidation (Admin.tsx, routers/users.py):
- Users tab Role/Department become read-only labels (Status + Actions remain);
the Roles tab "add someone" now sets role + department together.
Verified: feed 31/31, notifications 8/8, playwright 13/13; tsc -b + build +
py_compile clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dehx6goqDV2TiQ92dFFWGP
- design-library-feed.md / design-library-plan.md: the "OS for design" home-feed
plan + reconciliation.
- wfx-oc-api-dummy-review-2026-07-16.md: field-by-field review of WFX's dummy JSON;
grain settled (OC = whole order, deliveries nested), StyleID/OrderCode added,
history via date-range, docs via temporary per-OC SharePoint URL (download+keep),
Saved-OC handling = Pratik's Option A (track from email, lot number links later).
- STATUS.md + tna-governance-platform-plan.md: reflect the above.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dehx6goqDV2TiQ92dFFWGP
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>
- plan_drift service: measure each milestone against our frozen TnaRecommendation
schedule (joined by normalized task name), surfaced as a per-order 'vs approved
plan' banner + per-milestone 'N days behind plan' — distinct from Stitch's own delay.
- tna_export: resolve task names to the master's canonical spelling (normalized
match) so naming variants (PCD ( Plan Cut Date ), SEWING START) auto-align in the
push payload instead of failing category resolution.
- stitch_service: monotonic completion guard — a flaky Stitch payload that drops
complete_date can no longer un-tick a milestone the floor already finished.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sprint-0 items from Pratik's governance direction (all zero-decision, unblocked):
- Corpse-archive filter: services/wfx_service.orders_without_tna hides orders >120d
overdue (CORPSE_MAX_OVERDUE_DAYS, exclude_corpses param) so red stays meaningful —
scoped to /tna/no-tna (all-orders unaffected); response returns an `archived` count.
Also raised the /no-tna cap to 500 so the worklist has the full set to paginate.
- Plain-language milestone names: utils/tnaLabels.milestoneLabel maps raw Stitch jargon
("Bulk Flow Start IH Date") + Category-template names to merchant-readable labels;
applied to Create-TNA, LifecycleTimeline, and PERT (raw kept as a hover tooltip).
- Orders needing a plan: 5-per-page pagination (urgency bands become inline headers)
+ an "N archived" note next to the count.
- docs: governance platform plan (synthesis of the 9-Jul blueprint) + STATUS update.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the buyer-name fuzzy match with Sharavana's real model. The plan is chosen
by value-add category × fabric type; lead time is deterministic from the grid; the
engine selects the real synced Stitch template ("Category N - X Days") and back-
schedules its authoritative milestones (no hardcoding — Sharavana maintains them in
Stitch, we re-sync). services/tna_catalog.py holds only what's not in Stitch: the
(category × fabric) → tier grid + the WFX→category/fabric pre-fill guessers.
- recommend(): select "Category {N} - {gridLead} Days" mirror template, schedule_backward
its items; encoded catalog kept only as a fallback if a template hasn't synced.
- _resolve_order_context: returns garment/category/fabric pre-fill guesses from WfxOrder.
- POST /tna/draft: takes category/fabric_type/garment (defaulting to the guesses);
push builds from the selected mirror template. TnaRecommendation gains
category/fabric_type/garment (self-healed).
- Frontend TnaCreate: two dropdowns (value-add, fabric) + auto-derived lead, replacing
the template picker + free-text lead box.
- docs/plans/STATUS.md: P1/P2 progress + infra lessons.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI runs `tsc -b` (project-reference build), stricter than the tsc --noEmit I'd been
checking with — it caught two errors from the worklist refactor that silently failed
lint-and-test and skipped every deploy since:
- TNALine.tsx: leftover loadNoTna(noTnaSort) in the sync handler (noTnaSort removed).
- TnaToPlan.tsx: unused GREEN constant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The concurrency key likely isn't parsed by this Gitea runner, so the prior deploy
run didn't execute. Remove it; the reset --hard origin/uat (force host repo to
remote) is the actual fix for the stuck checkout serving a stale bundle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rapid pushes to uat ran the deploy concurrently (no concurrency guard), racing the
same containers/host /code repo and leaving UAT serving a stale bundle:
- Add a concurrency group (cancel-in-progress:false) so deploys queue, never overlap.
- Replace `git merge origin/uat` with `git checkout -f uat && git reset --hard
origin/uat` so the host repo always matches remote exactly, self-healing a stuck
checkout from a collided run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The Stitch tab is an integration/ops diagnostic, not a merchant/planner workflow;
show it only to admins (can('users','view'), the app's admin convention).
- Update the Template Readiness subtitle — the write-back is live now; the page is
the gauge for when Stitch seeds the remaining tasks into the tenant (drops the
stale "write endpoint isn't live yet").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The item lived at /tna/to-plan, so the TNA rail link (to:/tna, no `end`) matched it
as a child and both highlighted. Moving it out of the /tna namespace to /to-plan
keeps TNA active for /tna/* and To Plan active for /to-plan, with no overlap — and
without regressing TNA's highlight on /tna/all, /tna/board, etc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Give the worklist its own left-sidebar entry ("To Plan") instead of nesting it
as a sub-tab of Production Line; remove the "Plan" tab from the PL tab strip
(the count banner on the Line stays as a shortcut).
- Dedupe the WFX no-TNA feed by the plannable key (lot||order_code) so an order
with multiple PO-line splits shows once, keeping the most urgent line — fixes
the same order appearing several times and a non-unique React key.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the cramped no-TNA card grid at the bottom of the Production Line with a
dedicated, urgency-banded worklist (route /tna/to-plan):
- Bands: Due now (overdue/≤2wk) / Coming up (2-6wk) / Later, top band open.
- Mine-first by default (filters created_by against the logged-in user), with an
Everyone toggle + search; overdue rows get the darkest stripe and sort first.
- One clear "Create TNA →" per row; urgency encoded twice (stripe + countdown).
The Production Line keeps a compact count banner + a new "Plan" tab that link here;
the old NoTnaCard and its sort/show-all state are removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the merchant authoring flow on top of the existing recommender:
- POST /tna/draft: propose a TNA for a no-TNA order, with optional template
override + fabric-lead-time override (recommender extended for both).
- PUT /tna/recommend/{key}/schedule: persist planner date/owner edits (frozen
once accepted); sets edited flag.
- POST /tna/push/{key}: push an approved plan's template to Stitch, gated on
Template-Readiness — returns blocked + missing tasks when the tenant isn't
seeded (no silent failure). Distinct path avoids the /{decision} route clash.
- TnaRecommendation gains pcd_date/edited/stitch_template_id/push_status/
push_detail (idempotent migration + boot self-heal in services/tna_authoring).
- Frontend: TnaCreate page (proposal + template picker + fabric-lead-time input +
editable milestone table + approve→push with readiness gate), wired from the
"Create TNA" action on the no-TNA cards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
UAT backend needs MISTRAL_API_KEY and other credentials at startup.
Added them with defaults directly in environment: section since
.env.uat file is gitignored and won't exist in Dokploy checkouts.
Dokploy env vars take precedence when set via Environment Variables UI.
- CORS_ORIGINS and QR_BASE_URL still pointed to wrong domain (uat.sapl.*)
- Removed env_file: ./.env.uat — file is gitignored so Dokploy clone lacks it
- Secrets now set via Dokploy Environment Variables UI (same as production)
- Local/CI can use: docker compose --env-file .env.uat
The Gitea Actions runner doesn't expose Docker socket to job containers,
so Docker-in-Docker deploy pattern was failing silently. Replaced with
a host-side cron watcher (uat-auto-deploy.sh) that runs every 5 minutes,
checks for new commits on uat branch, and deploys directly on the host.
CI workflow now only does lint+test for quality gates on PRs.