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>
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>
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>
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>
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>
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>
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>
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 "Sync now" button on the Production Line that pulls WFX on demand and
refreshes the board, so a freshly-created lot appears in seconds instead of
waiting for the timer. Drop the WFX mirror poll from 2 min to 30s as the
no-click safety net. Lower the /wfx-sync gate from tna:manage to tna:view —
it's a benign read-refresh, so any TNA user (e.g. a merchant who just made the
lot) can trigger it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The token-signing secret_key must be overridden in production; running on the
built-in default makes auth tokens forgeable. Log a CRITICAL on boot so this
can't happen silently.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Recommendation engine: mirror Stitch's 17 task templates, fuzzy-match the
best template per order (buyer + lead-time fit), and back-schedule every
milestone date from the delivery anchor (CPM). Proposed plans stored in SAPL
(no Stitch write-back); planners review/accept on the order lifecycle page,
and "Waiting for a TNA" cards surface a "Suggested TNA ready" hint.
Critical milestones: drive the key/critical flag from Stitch's own
critical_task flag (no duplicate definition); flag them in PERT + order
detail, add a clickable milestone detail card, a key-dates strip and a
key-only focus toggle in the timeline.
Critical Dates board (/tna/key-dates): cross-order table of every order's
critical milestones, plus a month-grid calendar view of what's due when.
Added a consistent Line/All/Critical/Dashboard nav across the TNA pages.
Also: map the spaced "PCD ( Plan Cut Date )" task variant to D2; restore +
extend the TNA smoke test (auth + recommendation assertions), 48/48 passing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Base.metadata.create_all can't ALTER existing tables, so a prod DB that
predates the RBAC columns would break on boot. ensure_rbac_schema() adds the
missing department_id/is_active columns idempotently on every startup, run
right after create_all and before seed_rbac.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes#1
Scraper now runs entirely on Gemini (gemini-3-flash-preview):
- scraper_agent.py: agent loop rewritten to Gemini native function-calling
(Tool/FunctionDeclaration from the existing schema, tool_config AUTO,
function_response parts fed back, _to_jsonable arg coercion, 8192 out-tokens
+ 1024 thinking so the report arg never truncates, fail-fast if no key)
- scraper_tools._analyze_image: Pixtral -> Gemini vision (JSON, thinking_budget=0)
- same 4 tools, max_steps=10, 429-retry, 2s/5s pacing preserved
- Groq/Mistral still power garment tagging + PERT narrator (untouched)
- requires only GEMINI_API_KEY; verified live end-to-end (Zudio SS26, 14 items)
Also fixes found while running locally on a fresh clone:
- client.ts: imageUrl/scrapedImageUrl derive ASSET_ORIGIN from API BASE so
images resolve to the backend in dev; prod output byte-identical (/images,
/scraped relative, proxied by nginx)
- main.py: create data/images + data/scraped before StaticFiles mounts (mount
runs at import, before the lifespan hook) so a fresh checkout boots
- Scraper.tsx: prominent loading state (spinner + current action + elapsed
timer + "Starting agent..." panel)
Docs: STATUS.md log entry + stack/scraper references updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>