# VASTRA — Principal Architecture Plan

**Domain:** `https://vastra.clyro.sbs` · **Docroot:** `/home/clyrosbs/vastra.clyro.sbs` (currently empty except `.well-known`, `cgi-bin`)
**Stack:** React (Vite) static SPA + Express REST API · **Currency:** INR · **Market:** India
**Verified environment (2026-09-20):** Node `v22.23.1` + npm `10.9.8` at `/opt/alt/alt-nodejs22/root/usr/bin` ✓ · MariaDB `10.11.18` client ✓ · DB `clyrosbs_vastra` exists, **0 tables** ✓ · port 4120 free ✓ · disk 1.4T avail ✓ · Apache modules not checkable without root (assume `mod_rewrite`+`mod_proxy` `[P]` works per host note; ws:// does NOT work → no websockets anywhere).

## 0. Global Hard Constraints (binding on all workers)

1. `export PATH=/opt/alt/alt-nodejs22/root/usr/bin:$PATH` before every node/npm command. No `sudo`, no `npm i -g`; all deps in local `node_modules`.
2. Backend runs as plain `node server.js`, bound to `127.0.0.1:4120` only (never `0.0.0.0`, never another port). Kept alive via `nohup` + `keep.js` cron pattern (see §1.4). No PM2, no systemd, no Docker.
3. Frontend reverse-proxy via docroot `.htaccess` RewriteRules with `[P]` flag for `/api/*` → `127.0.0.1:4120`. No websockets, no SSE, no Socket.io — only plain HTTP `fetch` with short polling (e.g. order-track page polls every 10 s, stops on delivered/cancelled).
4. No external paid APIs. No hotlinked images — all product imagery is pure CSS/SVG placeholders generated in-repo (deterministic gradient + motif per product, see §4.6). No Google Fonts CDN dependency at runtime (bundle fonts or use system stack; Vite may self-host via `@fontsource` local packages).
5. DB credentials: host `localhost`, db `clyrosbs_vastra`, user `clyrosbs_vastra`, password `Vx2026!Secure#App$09`. Never commit password to git-tracked frontend code; backend reads from `api/.env` (chmod 600, never served — `.htaccess` denies it) with fallback only in `api/config.js`.
6. MariaDB = MySQL-compatible. Use `mysql2` driver, `utf8mb4` everywhere, `InnoDB`. All money in **integer paise** (`INT`, ₹1 = 100) to avoid float errors; format to INR only at display.

---

## 1. Final Architecture

### 1.1 Topology (single shared-cPanel host)

```
Browser ──HTTPS──▶ vastra.clyro.sbs (Apache/cPanel, docroot)
   │                    │
   │                    ├─ /* (static) ──▶ /index.html + /assets/* (Vite build output, copied to docroot root)
   │                    └─ /api/* ──▶ [P] proxy ──▶ 127.0.0.1:4120 (Express, plain node server.js via nohup)
   │                                                     │
   └─────────────────────────────────────────────────────┘  (plain fetch polling only)
                                                     Express ──▶ localhost MariaDB clyrosbs_vastra (mysql2 pool)
```

- SPA routing is hash-free `BrowserRouter`-compatible via Apache fallback: every non-file, non-`/api` request rewrites to `/index.html` (SPA fallback rule placed AFTER the `/api` proxy rule).
- API base from browser: same-origin `/api` (no CORS needed). Admin UI is part of the same SPA under `/admin` (token in `localStorage`, sent as `Authorization: Bearer`).
- No server-side rendering, no edge functions, no cron-driven builds. Two deploy artifacts only: (a) static files in docroot, (b) `api/` directory + running process.

### 1.2 Exact Folder Layout (docroot = repo root)

```
/home/clyrosbs/vastra.clyro.sbs/
├── .htaccess                 # proxy /api → 127.0.0.1:4120 + SPA fallback + hardening (frontend worker owns; backend reviews)
├── index.html                # BUILT file (Vite output, do not hand-edit; source is src-web/index.html)
├── assets/                   # BUILT files (Vite output: assets/index-[hash].js/.css)
├── vite.svg                  # Vite placeholder (may delete)
├── PLAN.md                   # this file
├── src-web/                  # FRONTEND SOURCE (Vite project root)
│   ├── package.json
│   ├── vite.config.js        # build: outDir='..' (docroot), emptyOutDir=false carefully scoped, base='/'
│   ├── index.html            # source HTML
│   └── src/
│       ├── main.jsx, App.jsx, api.js (fetch wrapper), store.jsx (cart context + localStorage)
│       ├── components/ (Navbar, Footer, ProductCard, Artwork.jsx (SVG placeholder engine), Filters, AdminTable, Toast)
│       └── pages/ (Home, Shop, Product, Cart, Checkout, OrderSuccess, Track, AdminLogin, AdminDashboard, AdminOrders, AdminProducts, NotFound)
├── api/                      # BACKEND SOURCE (Express project)
│   ├── package.json
│   ├── .env                  # DB_HOST/DB_USER/DB_PASS/DB_NAME/PORT=4120/ADMIN_SEED_*, chmod 600 (NEVER copy to docroot-served path)
│   ├── server.js             # entry: app.listen(4120,'127.0.0.1'); owns middleware order, /api/health
│   ├── config.js             # env loader + mysql2 pool (connectionLimit 5, queueLimit 0)
│   ├── schema.sql            # canonical DDL (database worker owns; backend executes, never drifts)
│   ├── seed.sql              # canonical seed INSERTs (12+ products etc.)
│   ├── keep.js               # self-ping/restart helper invoked from cron (HTTP GET /api/health, re-launch nohup if down)
│   ├── middleware/auth.js    # admin JWT (jsonwebtoken) verify; demo: 24h expiry, bcryptjs hash
│   ├── routes/
│   │   ├── shop.js           # public catalog/cart-support/order/review/coupon-validate/track endpoints
│   │   └── admin.js          # /api/admin/* (login, CRUD, orders, coupons, reviews, stats)
│   └── utils/ (validators.js, pricing.js — paise math, coupon engine, order-status state machine)
├── logs/                     # api.log, keep.log (nohup output; never served — Deny in .htaccess)
└── tmp/                      # build scratch (never served)
```

Key rules: `src-web/node_modules` and `api/node_modules` are local per project. Vite `outDir` writes directly to docroot root — `emptyOutDir` must be FALSE or scoped so it never deletes `api/`, `logs/`, `.htaccess`, `PLAN.md`, `cgi-bin`. `api/.env` and `logs/` blocked by `.htaccess` (`Require all denied` / `RewriteRule ^(api|logs|tmp)/ - [F]` belt-and-braces).

### 1.3 `.htaccess` Contract (exact behavior, frontend worker implements)

Order matters: (1) `RewriteEngine On`; (2) `/api/*` → `http://127.0.0.1:4120/api/* [P,L]` (preserve query string `QSA`); (3) existing files/dirs served directly (`RewriteCond %{REQUEST_FILENAME} -f [OR] / -d`, skip); (4) everything else → `/index.html [L]` for SPA routes (`/shop`, `/product/:slug`, `/admin`, …). Plus: `Options -Indexes`, deny dotfiles except `.well-known` (LetsEncrypt renewal must keep working), deny `api/.env`, `logs/`, `tmp/`, force HTTPS (only if host provides cert; keep behind a condition that doesn't loop on health checks).

### 1.4 Process Supervision (no-websocket-safe)

- Start: `cd ~/vastra.clyro.sbs/api && export PATH=... && nohup node server.js >> ../logs/api.log 2>&1 &`.
- `keep.js`: every 5 min via cPanel cron `node /home/clyrosbs/vastra.clyro.sbs/api/keep.js >> /home/clyrosbs/vastra.clyro.sbs/logs/keep.log 2>&1`; it `fetch('http://127.0.0.1:4120/api/health')`; on fail/timeout it spawns `node server.js` with `detached:true, stdio:append to logs/api.log`. Lockfile `tmp/keep.lock` prevents concurrent restarts. QA verifies kill-and-revive.
- Backend exposes `GET /api/health` (no auth, returns `{ok:true, time, db:'up'|'down', version}`) used by keep.js and QA smoke tests.

---

## 2. Full DB Schema (MariaDB 10.11 / InnoDB / utf8mb4_unicode_ci)

Conventions: `id INT UNSIGNED AUTO_INCREMENT PK`; money `INT` paise; timestamps `created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`, `updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`; `status` fields are `ENUM` or `VARCHAR(24)` with app-level state machine; soft-delete products via `is_active TINYINT(1) DEFAULT 1` (never hard-delete products with orders). All FKs `ON UPDATE CASCADE`, `ON DELETE RESTRICT` except `order_items→orders CASCADE`, `product_images/variants→products CASCADE`.

| Table | Columns (type, constraints) | Indexes |
|---|---|---|
| `categories` | `id` PK · `name VARCHAR(60) UNIQUE NOT NULL` (Kurta, Saree, Shirt, Jeans, Ethnic, Western) · `slug VARCHAR(60) UNIQUE NOT NULL` · `description VARCHAR(255)` · `image_hint VARCHAR(60)` (motif key for SVG engine, e.g. `paisley`) · timestamps | `UNIQUE(name)`, `UNIQUE(slug)` |
| `products` | `id` PK · `category_id INT UNSIGNED NOT NULL FK→categories(id)` · `name VARCHAR(120) NOT NULL` · `slug VARCHAR(140) UNIQUE NOT NULL` · `description TEXT` · `fabric VARCHAR(60)` (Cotton/Silk/Georgette/Denim/…) · `price_mrp INT NOT NULL` (paise) · `price_sale INT NOT NULL` (paise, ≤ mrp) · `rating_avg DECIMAL(2,1) DEFAULT 0.0` · `rating_count INT DEFAULT 0` · `is_active TINYINT(1) DEFAULT 1` · `is_featured TINYINT(1) DEFAULT 0` · `created_at/updated_at` | `UNIQUE(slug)`, `INDEX(category_id)`, `INDEX(is_active)`, `INDEX(is_featured)`, `FULLTEXT(name,description)` |
| `product_images` | `id` PK · `product_id INT UNSIGNED NOT NULL FK→products(id) ON DELETE CASCADE` · `sort_order TINYINT DEFAULT 0` · `palette CHAR(7) NOT NULL` (base hex, e.g. `#7A1E2B`) · `motif VARCHAR(40) DEFAULT 'paisley'` · `label VARCHAR(80)` (alt text) | `INDEX(product_id, sort_order)` |
| `product_variants` | `id` PK · `product_id INT UNSIGNED NOT NULL FK→products(id) ON DELETE CASCADE` · `size ENUM('S','M','L','XL','XXL','FS') NOT NULL` (`FS`=free-size for sarees) · `sku VARCHAR(40) UNIQUE NOT NULL` (e.g. `VST-KUR-001-M`) · `stock INT NOT NULL DEFAULT 0` · timestamps | `UNIQUE(sku)`, `UNIQUE(product_id,size)`, `INDEX(product_id)` |
| `customers` | `id` PK · `name VARCHAR(100) NOT NULL` · `phone VARCHAR(15) NOT NULL` (10-digit Indian, validated `^[6-9]\d{9}$`) · `email VARCHAR(120) NULL` · `address_line VARCHAR(200) NOT NULL` · `city VARCHAR(60)` · `state VARCHAR(60)` · `pincode CHAR(6) NOT NULL` (validated `^[1-9]\d{5}$`) · timestamps | `INDEX(phone)`, `INDEX(pincode)` |
| `orders` | `id` PK · `order_no VARCHAR(16) UNIQUE NOT NULL` (`VST` + 6-digit seq, e.g. `VST100231`) · `customer_id INT UNSIGNED NOT NULL FK→customers(id)` · `subtotal INT NOT NULL` · `discount INT DEFAULT 0` · `shipping INT DEFAULT 0` (0 if subtotal≥99900 paise else 7900) · `total INT NOT NULL` (paise) · `payment_method ENUM('COD','UPI') NOT NULL` · `payment_status ENUM('PENDING','PAID','FAILED') DEFAULT 'PENDING'` · `coupon_code VARCHAR(30) NULL` · `status ENUM('PLACED','CONFIRMED','SHIPPED','DELIVERED','CANCELLED') DEFAULT 'PLACED'` · `upi_ref VARCHAR(40) NULL` (demo UPI txn id) · timestamps | `UNIQUE(order_no)`, `INDEX(customer_id)`, `INDEX(status)`, `INDEX(created_at)` |
| `order_items` | `id` PK · `order_id INT UNSIGNED NOT NULL FK→orders(id) ON DELETE CASCADE` · `product_id INT UNSIGNED NOT NULL FK→products(id)` · `variant_id INT UNSIGNED NULL FK→product_variants(id)` · `size VARCHAR(5) NOT NULL` · `qty TINYINT UNSIGNED NOT NULL` · `unit_price INT NOT NULL` (paise snapshot) · `line_total INT NOT NULL` | `INDEX(order_id)`, `INDEX(product_id)` |
| `reviews` | `id` PK · `product_id INT UNSIGNED NOT NULL FK→products(id) ON DELETE CASCADE` · `author VARCHAR(80) NOT NULL` · `rating TINYINT NOT NULL CHECK 1–5` · `title VARCHAR(120)` · `body TEXT` · `is_approved TINYINT(1) DEFAULT 0` · timestamps | `INDEX(product_id)`, `INDEX(is_approved)` |
| `coupons` | `id` PK · `code VARCHAR(30) UNIQUE NOT NULL` (uppercase) · `type ENUM('FLAT','PCT') NOT NULL` · `value INT NOT NULL` (paise if FLAT, 1–90 if PCT) · `min_order INT DEFAULT 0` (paise) · `max_discount INT NULL` (paise cap for PCT) · `usage_limit INT NULL` · `used_count INT DEFAULT 0` · `is_active TINYINT(1) DEFAULT 1` · `starts_at/ends_at DATETIME NULL` · timestamps | `UNIQUE(code)`, `INDEX(is_active)` |
| `admin_users` | `id` PK · `username VARCHAR(40) UNIQUE NOT NULL` · `password_hash VARCHAR(255) NOT NULL` (bcrypt, cost 10) · `created_at` | `UNIQUE(username)` |

Notes: `rating_avg/count` are denormalized counters updated transactionally on review approval (DB worker owns trigger-or-app-update decision; app update preferred, single `UPDATE products` in same txn). Stock decrement is atomic (`UPDATE product_variants SET stock = stock - ? WHERE id=? AND stock >= ?`, check affectedRows) inside the order-creation transaction to prevent oversell. No FK from `orders.coupon_code` (keep denormalized string; validate at order time).

### Seed Data Plan (in `seed.sql`, idempotent via INSERT … ON DUPLICATE KEY UPDATE or guarded re-runnable script)

- 6 categories: Kurta (`kurta`), Saree (`saree`), Shirt (`shirt`), Jeans (`jeans`), Ethnic (`ethnic`), Western (`western`) with descriptions + motif hints.
- 12 products (2 per category), INR pricing in paise, realistic MRP→sale: e.g. Banarasi Silk Saree ₹4,999→₹3,499; Handloom Cotton Kurta ₹1,999→₹1,299; Oxford Casual Shirt ₹1,499→₹999; Slim-Fit Denim Jeans ₹2,499→₹1,799; Anarkali Ethnic Set ₹3,999→₹2,799; Floral Summer Dress (Western) ₹2,199→₹1,499; plus 6 more (Linen Shirt, Straight Jeans, Chikankari Kurta, Georgette Saree, Nehru Jacket-Ethnic, Co-ord Western set). Each: fabric, description (2–3 lines, Indian context), `is_featured` on 4 (homepage).
- `product_images`: 2 rows per product (24 rows), distinct `palette`+`motif` pairs driving the SVG engine (no binary blobs).
- `product_variants`: apparel sizes S/M/L/XL/XXL stock 8–25 each; sarees single `FS` row stock 15–30. SKUs deterministic `VST-<CAT3>-<NNN>-<SIZE>`.
- 3 coupons: `WELCOME10` (PCT 10, cap ₹500, min ₹999), `FLAT200` (FLAT ₹200, min ₹1,499), `FESTIVE15` (PCT 15, cap ₹1,000, min ₹2,999, date-bounded).
- 1 admin: username `admin`, password from `ADMIN_SEED_PASSWORD` env (default `Vastra2026!Admin`, forced-change note in admin UI placeholder), bcrypt-hashed at seed time.
- ~8 approved reviews spread across featured products (ratings 4–5, Indian names) + 2 unapproved (moderation demo). Rating counters precomputed consistently.

---

## 3. REST API Contract (all under `/api`, JSON, UTF-8)

Conventions: success `{ "ok": true, "data": … }`; error `{ "ok": false, "error": { "code": "STRING", "message": "…" } }` with HTTP 400/404/409/422/401/500. Pagination `?page=1&limit=12` → `{items, page, limit, total, pages}`. Admin auth: `POST /api/admin/login` → JWT; all other `/api/admin/*` require `Authorization: Bearer <jwt>` (401 `UNAUTH`). IDs numeric; money paise ints; no websockets.

### 3.1 Shop (public, no auth)

| Method+Path | Req | Resp (200) | Notes |
|---|---|---|---|
| `GET /api/health` | — | `{ok:true, data:{status:'up', db:'up', time, version:'1.0.0'}}` | keep.js + QA ping; DB check via `SELECT 1`. |
| `GET /api/categories` | — | `{ok:true, data:[{id,name,slug,description,product_count}]}` | `product_count` = active products. |
| `GET /api/products?page&limit&category(slug)&search&sort(new|price_asc|price_desc|rating)&min_price&max_price(prices in paise)&featured=1` | query | paginated `[{id,slug,name,category,price_mrp,price_sale,discount_pct,rating_avg,rating_count,primary_image:{palette,motif},sizes:[…],in_stock}]` | `in_stock` = any variant stock>0. |
| `GET /api/products/:slug` | — | `{…, description,fabric,images[],variants:[{id,size,sku,stock}],reviews Approved:[{author,rating,title,body,created_at}], related:[4 same-category]}` | 404 `PRODUCT_NOT_FOUND`. |
| `POST /api/coupons/validate` | `{code, subtotal}` (paise) | `{ok:true, data:{valid:true, code, discount, payable}}` or `{valid:false, reason}` | Pure computation, no side effects; checks active/window/limit/min_order + cap. |
| `POST /api/orders` | `{customer:{name,phone,email?,address_line,city,state,pincode}, items:[{variant_id,qty}], payment_method:'COD'\|'UPI', upi_ref?, coupon_code?}` | `201 {ok:true, data:{order_no, total, status:'PLACED', eta_date}}` | Txn: validate phone/pincode, lock+decrement stock atomically (409 `OUT_OF_STOCK` w/ item detail), recompute prices server-side (never trust client totals), apply coupon + `used_count++`, shipping rule, insert customer (reuse by phone? create new row per order — simpler, decided) + order + items. UPI is demo: accept any `upi_ref` ≥6 chars; `payment_status`=`PENDING` for COD, `PAID` placeholder for UPI demo. |
| `GET /api/orders/:orderNo` | — | `{order_no,status,payment_method,payment_status,totals{…},items[{name,size,qty,unit_price}],timeline:[{status,at}], eta_date}` | Public tracking (no auth; order_no is unguessable enough for demo). 404 `ORDER_NOT_FOUND`. Poll every 10 s max. |
| `GET /api/reviews?product_id` | query | approved reviews list | Public read. |
| `POST /api/reviews` | `{product_id, author, rating 1–5, title?, body?}` | `201 {ok:true, data:{id, message:'Thanks! Awaiting moderation.'}}` | Creates `is_approved=0`; does NOT touch counters. 422 on validation. |

### 3.2 Admin (all except login require Bearer JWT)

| Method+Path | Req | Resp | Notes |
|---|---|---|---|
| `POST /api/admin/login` | `{username, password}` | `{ok:true, data:{token, username}}` | bcrypt compare, 401 `BAD_CREDENTIALS`; JWT 24 h. |
| `GET /api/admin/stats` | — | `{revenue_paise, orders_today, orders_total, low_stock_count, pending_reviews, by_status{…}}` | Single dashboard call. |
| `GET /api/admin/orders?page&limit&status` | query | paginated orders w/ customer + item counts | |
| `GET /api/admin/orders/:id` | — | full order + customer + items | |
| `PATCH /api/admin/orders/:id` | `{status}` | updated order | State machine: PLACED→CONFIRMED→SHIPPED→DELIVERED; any→CANCELLED (restores stock in txn); reject backward jumps 422 `BAD_TRANSITION`. |
| `GET /api/admin/products?page&limit&search` | query | products w/ stock totals | |
| `POST /api/admin/products` | `{name,slug?,category_id,description,fabric,price_mrp,price_sale,is_featured,images[{palette,motif}],variants[{size,stock}]}` | `201` created product | Slug auto-generated if omitted; txn across 3 tables. |
| `PUT /api/admin/products/:id` | same shape | updated product | Replaces images/variants sets in txn (variants with order history keep rows, stock merged). |
| `PATCH /api/admin/products/:id/stock` | `{variant_id, stock}` | updated variant | Quick stock fix. |
| `DELETE /api/admin/products/:id` | — | `{ok:true}` | Soft-delete (`is_active=0`); 409 if open orders contain it (check). |
| `GET /api/admin/reviews?pending=1` | query | queue incl. product names | |
| `PATCH /api/admin/reviews/:id` | `{is_approved:0\|1}` | updated | Approval updates `products.rating_avg/count` in same txn; un-approve reverses. |
| `GET /api/admin/coupons` | — | list all | |
| `POST /api/admin/coupons` | `{code,type,value,min_order,max_discount,usage_limit,starts_at,ends_at,is_active}` | `201` | Uppercase code, validate ranges. |
| `PATCH /api/admin/coupons/:id` | partial | updated | |
| `DELETE /api/admin/coupons/:id` | — | `{ok:true}` | Hard delete allowed only if `used_count=0`, else deactivate. |

Error codes registry (shared): `BAD_INPUT, UNAUTH, PRODUCT_NOT_FOUND, ORDER_NOT_FOUND, OUT_OF_STOCK, BAD_COUPON, BAD_TRANSITION, BAD_CREDENTIALS, CONFLICT, DB_ERROR`.

---

## 4. Design System — Premium (non-generic) UI

### 4.1 Brand & Palette (deep maroon + gold + ivory; NOT purple/blue gradients)

CSS vars: `--ink:#2A1A12` (espresso text) · `--maroon:#7A1E2B` (primary) · `--maroon-deep:#5C1420` (hover/footer) · `--gold:#C9A227` (accents, dividers, star ratings) · `--gold-soft:#EAD9A6` · `--ivory:#FAF6EE` (page bg) · `--card:#FFFFFF` · `--sage:#5F6C5D` (secondary text/success adj) · `--line:#E8DFC F`→`#E8DFCF` (borders) · `--danger:#B3261E`. Dark surfaces: footer/maroon-deep with ivory text only. Gold used sparingly (rules, badges, CTAs hover ring) — never large fills. Contrast: maroon-on-ivory ≥ 7:1 for body.

### 4.2 Typography (self-hosted, no runtime CDN)

Display: Fraunces (serif, 600/700) for H1/H2/brand wordmark via `@fontsource` local bundle; Body/UI: Inter or system stack (`-apple-system,'Segoe UI',Roboto,'Noto Sans',sans-serif`). Scale: H1 32/40 mobile → 48/56 desktop; H2 24→32; body 15–16, line-height 1.6; prices tabular-nums, formatted `Intl.NumberFormat('en-IN',{style:'currency',currency:'INR'})`. Devanagari accent word (e.g. वस्त्र) allowed in logo lockup only.

### 4.3 Spacing, Shape, Motion

4-pt base (`--s1:4px … --s8:32px`); container max 1200px, gutters 16px mobile / 24px desktop. Radius: cards 14px, pills 999px, buttons 10px. Shadows: single soft `0 8px 24px rgba(42,26,18,.08)`; no neon. Motion: 160 ms ease-out hovers, image zoom 1.03 on card hover; page fade-in only; `prefers-reduced-motion` disables. Focus-visible gold outline everywhere (a11y).

### 4.4 Components (built once in `components/`, reused)

`Navbar` (sticky, ivory blur, maroon wordmark, category links, search icon, cart badge count) · `Footer` (maroon-deep, 4 cols: shop/help/contact/UPI-COD note, pincode-serviceability line) · `ProductCard` (SVG Artwork, name, fabric, price pair MRP-strike + sale + %off badge gold, rating stars, Add button) · `Artwork.jsx` (deterministic SVG: base palette + motif `paisley|jaali|stripe|bandhani|checks` + subtle texture; aspect 3:4; `<title>` alt) · `Filters` (category chips, size, price bands ₹<1000/1000–2500/>2500, sort) as sidebar desktop / collapsible drawer mobile · `QtyStepper`, `Price` (paise→INR), `Stars`, `Toast`, `Empty` states · `AdminTable` (dense, sticky header, status pills color-coded) · `StatusPill` (PLACED amber … DELIVERED green, CANCELLED red).

### 4.5 Pages & Routes (SPA, mobile-first)

| Route | Purpose & key blocks |
|---|---|
| `/` Home | Hero (editorial serif headline + maroon panel + featured saree/kurta artwork collage, CTA Shop) · gold rule · category tiles (6, SVG motifs) · Featured (4 products) · craft story strip · testimonials (reviews) · COD/UPI trust badges · newsletter (decorative, localStorage only). |
| `/shop` | Filter drawer + grid (2-col phone / 3 tablet / 4 desktop), search, sort, pagination, skeleton loaders, empty state. |
| `/product/:slug` | Gallery (SVG variants switcher), name/fabric/rating, price block, size selector (S–XXL/FS + size guide modal cm/in), stock indicator (Only X left ≤5), qty, Add/Buy, delivery ETA (5–7 days) + pincode check (regex only), tabs: Description / Reviews (form + list) / Shipping (COD+UPI demo note). Related 4. |
| `/cart` | Line items (artwork thumb, size, stepper, remove), coupon input + validate, bill (subtotal/discount/shipping/total), free-shipping progress bar (₹999 threshold), checkout CTA. Cart persists `localStorage`. |
| `/checkout` | 3 steps (Details → Payment COD/UPI radio + demo UPI id field → Review). Validates phone/pincode regex client + server recheck. Place order → `POST /api/orders` → redirect success. |
| `/order-success/:orderNo` | Confirmation, order no big, ETA, track link, clear cart. |
| `/track` (+ `/track/:orderNo`) | Order-no input + status timeline (5 steps), polling 10 s until terminal. |
| `/admin` login | Username/password → JWT localStorage. |
| `/admin/dashboard` etc. | Stats cards, orders table + status advance, products CRUD (incl. palette/motif picker + variant grid), reviews moderation queue, coupons CRUD. Guard: redirect `/admin` if no token; 401 → logout. |
| `*` NotFound | Branded 404 with shop link. |

### 4.6 Breakpoints

`--bp-phone: ≤640px` (single col, bottom-pad CTA, drawer filters, hamburger) · `--bp-tablet: 641–1024px` (2–3 col grid, side filters collapsible) · `--bp-desktop: ≥1025px` (full 4-col, sticky filter rail, max 1200 container). Touch targets ≥44px; tap-safe size pills; no hover-dependent info (stock/price always visible).

### 4.7 Image Strategy (zero external requests)

`Artwork.jsx` renders inline SVG from `(palette, motif)` — 5 motifs hand-drawn paths; deterministic variant by product id. `product_images` rows store only palette+motif, so backend/frontend stay in sync without files. Favicon + OG image are local SVG. No `<img src=http…>` anywhere (QA greps).

---

## 5. Phased Task Split — 4 Workers, File-Level Ownership

### Worker A — Database (`api/schema.sql`, `api/seed.sql` ONLY)

1. Write `schema.sql` exactly per §2 (utf8mb4, InnoDB, FKs, indexes incl. FULLTEXT). 2. Write `seed.sql` per seed plan (6 cats, 12 products, 24 images, variants, 3 coupons, admin, 10 reviews w/ consistent counters). 3. Apply: `mysql -u… -p… clyrosbs_vastra < schema.sql`, then `seed.sql`; verify counts. 4. Hand `schema.sql` hash to B/C. **Do not touch** `server.js`, `src-web/*`, `.htaccess`.

### Worker B — Backend (`api/*` EXCEPT schema/seed.sql; reads them)

1. `package.json` (express, mysql2, jsonwebtoken, bcryptjs, dotenv, cors-off, helmet-lite, express-rate-limit on login/orders), `config.js`, `.env` (chmod 600). 2. `server.js` (+ `/api/health`), `middleware/auth.js`, `utils/{validators,pricing}.js`. 3. `routes/shop.js` then `routes/admin.js` per §3 contract (server-side price recompute, atomic stock txn, coupon engine, order state machine). 4. `keep.js` + start via nohup on 4120, cPanel cron line doc. **Do not touch** `src-web/*`, `.htaccess` (may propose snippet only).

### Worker C — Frontend (`src-web/*`, `.htaccess`, built `index.html`+`assets/*` ONLY)

1. Scaffold Vite React in `src-web/` with `outDir='..'`, `emptyOutDir:false`; `api.js` fetch wrapper (`/api` base, JSON, error codes, JWT attach), `store.jsx` cart. 2. `Artwork.jsx` + 5 motifs, design tokens CSS, shared components. 3. Shop pages (home→track per §4.5) wired to §3.1 with polling-only track. 4. Admin SPA per §3.2. 5. Write `.htaccess` per §1.3, build (`npm run build` outputs to docroot root), verify SPA fallback + `/api` proxy. **Do not touch** `api/*` (except reading contract), never emit websockets/SSE.

### Worker D — QA (NO source edits except `logs/*`-adjacent notes; files issues)

1. Contract tests: curl every §3 endpoint (happy + 404/422/409/401), verify paise math, coupon caps, oversell race (2× concurrent orders last-stock → exactly one 409). 2. E2E: home→shop→product→cart→checkout(COD+UPI)→success→track; admin login→CRUD→order advance→review approve; mobile 360px + desktop 1280px screenshots. 3. Perf/hardening: no external URLs (`grep -r http src-web/src`), `.env` not reachable via HTTP, SPA fallback on `/product/x`, keep.js kill-revive drill, Lighthouse-ish sanity. 4. Sign-off checklist below.

### Sequencing

Phase 1 (parallel): A schema draft + C design tokens/Artwork. Phase 2: A seed → B backend (against real DB). Phase 3: C frontend (against live `/api`). Phase 4: D full pass; B/C fix own files only. Integration point: `schema.sql` checksum + `/api/health db:up` gates Phase 3.

### Definition of Done (100% confidence — ALL must be ✓)

- [ ] `GET /api/health` → 200 `{db:'up'}` via public `https://vastra.clyro.sbs/api/health` (proxy works).
- [ ] DB has 10 tables, 6 categories, ≥12 active products, variants for S–XXL/FS, 3 coupons, admin login works.
- [ ] Shop flow: filter/search/sort/paginate, product page, cart persist, coupon validate, COD + UPI-demo orders create `VST######`, stock decrements, track page reaches PLACED with timeline.
- [ ] Admin flow: login JWT, stats load, order PLACED→…→DELIVERED + CANCELLED restores stock, product create/edit/soft-delete, review approve updates counters, coupon CRUD.
- [ ] Design: maroon/gold/ivory only, SVG-only imagery (zero external img), INR formatting, 360px + 1280px layouts clean, focus states visible.
- [ ] Ops: `node server.js` on 127.0.0.1:4120 via nohup, cron keep.js revives killed process <6 min, `.htaccess` blocks `api/.env` + `logs/`, no websockets in codebase (`grep -ri websocket|socket.io → ∅`).
- [ ] No secrets in frontend bundle or git; `logs/api.log` shows clean startup; PLAN.md + contracts match shipped behavior.

*Owner: Principal Architect subagent. Verified preconditions in §0 header. No code in this doc by design — workers implement.*
