Commit Graph
106 Commits
Author SHA1 Message Date
root 604e72dd43 chore(uat): add .env.uat.example + gitignore uat env file
- .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/
2026-07-08 11:50:56 +05:30
root 34b9929937 feat(uat): add UAT docker-compose + CI deploy workflow
- 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.
2026-07-08 11:48:46 +05:30
Ajay SableandClaude Opus 4.8 c6bf6efee5 feat(tna): Phase 0 + 2b — milestone source-map, reconciler, approval capture
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>
2026-07-07 11:53:53 +05:30
Ajay SableandClaude Opus 4.8 62793212b9 fix(nginx): re-resolve backend IP at runtime to stop 504 after redeploys
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>
2026-07-07 11:17:58 +05:30
Ajay SableandClaude Opus 4.8 ff63ee5991 fix(auth): bake public Google client id as config default so prod SSO shows
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>
2026-07-07 11:05:18 +05:30
Ajay SableandClaude Opus 4.8 615bb0f731 fix(auth): set GOOGLE_CLIENT_ID on prod so the Google SSO button shows
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>
2026-07-07 10:58:59 +05:30
Ajay SableandClaude Opus 4.8 147429379c feat(kaarigar): add OKF domain-knowledge layer to the assistant
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>
2026-07-05 14:11:54 +05:30
Ajay SableandClaude Fable 5 2ae3e425c2 feat(kaarigar): rebrand the assistant from HERMES to KAARIGAR + logo
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>
2026-07-03 15:38:32 +05:30
Ajay SableandClaude Opus 4.8 6751de114d feat(hermes): drive the Library's native facet filter, not a static list
"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>
2026-07-02 12:55:43 +05:30
Ajay SableandClaude Opus 4.8 6241ca45ee fix(hermes): precise library search via structured facet matching
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>
2026-07-02 12:42:45 +05:30
Ajay SableandClaude Opus 4.8 540e71b60d feat(hermes): UI-driving — navigate + highlight the thing, RPA-style
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>
2026-07-02 12:32:40 +05:30
Ajay SableandClaude Opus 4.8 281dfeb29b fix(hermes): surface the underlying LLM error (e.g. Gemini 403) instead of a generic message
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:12:19 +05:30
Ajay SableandClaude Opus 4.8 31665d64d7 feat(hermes): Ask HERMES command bar — read-only Q&A over Library + TNA
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>
2026-07-02 12:09:01 +05:30
Ajay SableandClaude Opus 4.8 288556047c feat(privacy): scope Trends/Moodboard/Boards to their creator
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>
2026-07-02 11:05:28 +05:30
Ajay SableandClaude Opus 4.8 58e6e80517 chore: trigger redeploy for role quick-add (7702110)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:40:05 +05:30
Ajay SableandClaude Opus 4.8 7702110686 feat(admin): quick-assign a role by email from the Roles tab
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>
2026-07-01 17:09:38 +05:30
Ajay SableandClaude Opus 4.8 8486079c56 feat(admin): password optional for company SSO accounts in user creation
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>
2026-07-01 16:59:08 +05:30
Ajay SableandClaude Opus 4.8 651d0c9bdf feat(auth): auto-provision company Google sign-ins with a limited default role
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>
2026-07-01 15:52:39 +05:30
Ajay SableandClaude Opus 4.8 3be60a301b feat(analytics): filters, library-growth trend, and expanded breakdowns
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>
2026-07-01 12:14:22 +05:30
Ajay SableandClaude Opus 4.8 9fb2571965 fix(auth): solid logout backdrop for text contrast; docs: STATUS update
- Logout overlay backdrop bg-paper/92 → solid bg-paper so the busy page
  (e.g. the colourful fabric grid) no longer bleeds through behind the dark
  "Signing out…" text.
- STATUS.md: add the 2026-06-30 session entry (fabric prod-seed + bulk/folder
  upload + 503/timeout retry + image-hash dedupe, concept fabric-picker +
  enhance, non-blocking startup, Lottie loaders, mobile bottom-nav, animated
  logout, WFX response, dormant Stitch TNA client).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:41:12 +05:30
Ajay SableandClaude Opus 4.8 9a1a27541a fix(auth): portal the logout overlay to body so it centres (and shows on mobile)
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>
2026-06-30 14:31:08 +05:30
Ajay SableandClaude Opus 4.8 68164fe74c fix(auth): play the logout animation full-screen before signing out
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>
2026-06-30 14:26:29 +05:30
Ajay SableandClaude Opus 4.8 a3adae5cdf feat(auth): animated Lottie sign-out button in the top bar
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>
2026-06-30 14:21:45 +05:30
Ajay SableandClaude Opus 4.8 7b6bb963f5 feat(mobile): bottom nav reaches all pages; drop the top hamburger
- 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>
2026-06-30 14:07:44 +05:30
Ajay SableandClaude Opus 4.8 22b0380d27 feat(mobile): bottom tab bar + responsive loaders/modals
- 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>
2026-06-30 13:34:26 +05:30
Ajay SableandClaude Opus 4.8 a385a8b506 feat(trends): centred research modal with the Searching animation
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>
2026-06-30 11:51:35 +05:30
Ajay SableandClaude Opus 4.8 742adf872d fix(startup): don't block app readiness on the initial integration syncs
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>
2026-06-30 11:36:15 +05:30
Ajay SableandClaude Opus 4.8 8bacbb1804 fix(gemini): retry timeouts/connection drops, not just 503
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>
2026-06-30 11:29:57 +05:30
Ajay SableandClaude Opus 4.8 24e0b62d3f feat(fabrics): centred upload modal with the animation + progress
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>
2026-06-30 11:21:00 +05:30
Ajay SableandClaude Opus 4.8 391fa46be4 feat(fabrics): Upload Lottie on the swatch-card bulk upload
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>
2026-06-30 11:17:48 +05:30
Ajay SableandClaude Opus 4.8 4831f72819 feat(ui): Lottie loaders for upload, trends & studio
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>
2026-06-30 11:13:39 +05:30
Ajay SableandClaude Opus 4.8 fa8a1d40fc fix(fabrics): dedupe swatch uploads by image hash
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>
2026-06-29 18:19:00 +05:30
Ajay SableandClaude Opus 4.8 d146d99908 fix(fabrics): survive Gemini 503 bursts during bulk upload
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>
2026-06-29 18:08:45 +05:30
Ajay SableandClaude Opus 4.8 8c4cb36f3d feat(fabrics): bulk swatch upload — multi-file + whole-folder select
"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>
2026-06-29 18:00:38 +05:30
Ajay SableandClaude Opus 4.8 335ba2b820 feat(stitch): scaffold parametrised TNA API client (dormant, flag-gated)
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>
2026-06-29 17:51:18 +05:30
Ajay SableandClaude Opus 4.8 3545e57181 fix(enhance): stop 502s — bigger token budget, strict JSON, retry on 503
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>
2026-06-29 16:35:30 +05:30
Ajay SableandClaude Opus 4.8 ed0f8715bc feat(concept-studio): ✦ Enhance prompt (reads garment + fabric + draft)
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>
2026-06-29 16:26:42 +05:30
Ajay SableandClaude Opus 4.8 2c34965d2c fix(concept-studio): select a library fabric by id (no cross-origin re-fetch)
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>
2026-06-29 16:19:00 +05:30
Ajay SableandClaude Opus 4.8 61454731cc fix(concept-studio): fabric-swatch picker reads the Fabric Library
"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>
2026-06-29 16:12:12 +05:30
Ajay SableandClaude Opus 4.8 5d044925c9 feat(fabrics): seed the Fabric Library on fresh deploys
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>
2026-06-29 16:04:31 +05:30
Ajay SableandClaude Opus 4.8 a4b737050d fix(library): typecheck failure blocking deploy
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>
2026-06-29 15:53:53 +05:30
Ajay SableandClaude Opus 4.8 11be606f8d feat(fabric-library): smart AI ingestion + premium page; library/nav polish
Fabric Library (new):
- Separate Fabric model/table; Gemini vision extractor reads the printed
  swatch-card spec label verbatim + analyses the cloth; Pillow swatch crop
- Bulk seeder (idempotent, timeout+retry+streaming); 59/214 seeded so far
- API: GET/facets/{id}, POST /upload, PATCH /{id} (verify)
- /fabrics page: swatch grid + Refine (reuses FacetSidebar) + digital-twin
  detail modal (edit/verify, Use-in-Studio, prev/next, download)

Garment Library detail modal: inline Edit (patchable columns + sample),
Use-in-Studio (as shape), prev/next; gated off the public scan view.

Nav: top tabs -> left hover-expand glass SideRail (shared lib/nav), slim
top bar, glassmorphism; Refine defaults closed; studios mobile-friendly.

Animation: Fabrics uniform row-major grid for a clean cascade; Library
keeps Pinterest masonry, fixed only the reveal (anim-fade-in, opacity-only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 13:42:51 +05:30
Ajay SableandClaude Opus 4.8 35d2fc0283 docs(hermes): addendum — elevate HERMES to the platform-wide master brain
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>
2026-06-26 12:55:43 +05:30
Ajay SableandClaude Opus 4.8 557e2672c3 security(login): remove "Use a demo account" link (+ hard-coded creds)
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>
2026-06-26 11:59:58 +05:30
Ajay SableandClaude Opus 4.8 caea2c66d3 fix(studio): Concept Studio layout fits the page (parity with Product Studio)
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>
2026-06-26 11:48:03 +05:30
Ajay SableandClaude Opus 4.8 340bd41a43 feat(studio): AI prompt enhancer for Product Studio
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>
2026-06-26 11:41:30 +05:30
Ajay SableandClaude Opus 4.8 e60eb0d186 fix(studio): Product Studio output quality — add HD/2K/4K (default 4K)
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>
2026-06-26 11:33:57 +05:30
Ajay SableandClaude Opus 4.8 79a92000b4 fix(studio): Product Studio layout fits the page
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>
2026-06-26 11:26:36 +05:30
Ajay SableandClaude Opus 4.8 93edc7f66f feat(studio): Product Studio Pro as a native proxy (our UI, their backend)
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>
2026-06-26 11:22:52 +05:30
Ajay SableandClaude Opus 4.8 372a7ac873 feat(studio): integrate Product Studio Pro (Phase 1 — gated embed)
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>
2026-06-26 11:13:29 +05:30