# MeshkeeApp — Project Context

> **For AI agents:** Read this file at the start of a new chat before making changes.  
> Update this document when a major feature is completed or architecture changes.

Last updated: August 5, 2026

---

## Repository layout

**Primary workspace:** open **`MeshkeeApp`** (this monorepo) as the Cursor workspace root.

```
MeshkeeApp/
  apps/
    super-admin/     # @meshkee/super-admin — port 5174 — meshkee.app
    business/        # @meshkee/business-dashboard — port 5173 — business.{domain}
    customer/        # @meshkee/customer-dashboard — port 5175 — customer.{domain}
  packages/
    dashboard-core/  # @meshkee/dashboard-core — API client, auth types, utils, tokens
    dashboard-ui/    # @meshkee/dashboard-ui — shared React components
  docs/              # This file + agent context
```

**Backend (separate repo):** sibling folder `MeshkeeApp Backend` — NestJS + Prisma + PostgreSQL. Not inside this monorepo.

**Stale workspace:** `MeshkeeApp Gen2` is empty/outdated — do not use it.

---

## Apps overview

| App | Package | Port | Local URL | Domain guard |
|-----|---------|------|-----------|--------------|
| Super Admin | `@meshkee/super-admin` | 5174 | `https://meshkee.app:5174` | `AdminDomainGuard` |
| Business | `@meshkee/business-dashboard` | 5173 | `http://business.sanihome.ir:5173` | `BusinessDomainGuard` |
| Customer | `@meshkee/customer-dashboard` | 5175 | `http://customer.sanihome.ir:5175` | `CustomerDomainGuard` |

### Shared packages migration

| App | `@meshkee/dashboard-core` | `@meshkee/dashboard-ui` |
|-----|---------------------------|-------------------------|
| Customer | ✅ wired | ✅ wired |
| Super Admin | ⏳ pending | ⏳ pending |
| Business | ⏳ pending | ⏳ pending |

When migrating an app to shared packages, prefer importing from `@meshkee/dashboard-core` / `@meshkee/dashboard-ui` instead of duplicating code.

---

## Tech stack

### All dashboard apps
- React 19, TypeScript, Vite, React Router
- CSS Modules + design tokens (`src/index.css`; customer also imports `@meshkee/dashboard-core/styles/tokens.css`)
- Auth via JWT (`src/context/AuthContext.tsx`, `src/services/authService.ts`)
- API client: `src/lib/api.ts` — base URL from `VITE_API_BASE_URL`

### Backend
- NestJS, Prisma, PostgreSQL, Redis
- S3-compatible storage (Parspack) for media
- API prefix: `/api/v1`
- BigInt IDs serialized as strings in JSON

---

## Local development

### 1. Monorepo setup

```bash
cd MeshkeeApp
npm install
```

### 2. Backend (sibling folder)

```bash
cd "../MeshkeeApp Backend"
cp .env.example .env   # fill DATABASE_URL, JWT secrets, S3 keys, GROQ_API_KEY (or OPENAI_API_KEY)
npm install
# Run SQL migrations in database/migrations/ (in order)
npx prisma generate
npm run start:dev      # default http://localhost:3000
```

### 3. Dashboard dev servers

From the monorepo root:

```bash
npm run dev:super-admin   # https://meshkee.app:5174
npm run dev:business      # http://business.sanihome.ir:5173
npm run dev:customer      # http://customer.sanihome.ir:5175
```

Or from an app directory:

```bash
cd apps/business && npm run dev
```

Copy each app's `.env.example` → `.env` before first run.

**Super Admin HTTPS:** generate local certs with mkcert (see `apps/super-admin/.env.example`).

### 4. Hosts file

Add to `/etc/hosts` (one line per tenant):

```
127.0.0.1 meshkee.app
127.0.0.1 business.sanihome.ir
127.0.0.1 customer.sanihome.ir
127.0.0.1 business.safeteb.com
127.0.0.1 customer.safeteb.com
127.0.0.1 safeteb.com
```

**Multi-tenant:** leave `VITE_BUSINESS_DOMAIN` unset in business/customer `.env`. Each dashboard build resolves the tenant from `window.location.hostname` (`business.safeteb.com` → `safeteb.com`). One dev server and one production deploy serve all businesses; the backend maps domain → business in the DB.

### 5. Test accounts

| App | Phone | Password | Notes |
|-----|-------|----------|-------|
| Business | `+989122222222` | `password` | sanihome.ir → **business_id `4`** |
| Customer | (same user) | `password` | customer.sanihome.ir tenant |
| Super Admin | (platform admin) | — | see backend seed / team records |

---

## App routes

### Super Admin (`apps/super-admin`)

| Path | Page |
|------|------|
| `/login` | Login |
| `/` | Home |
| `/businesses` | Businesses list (migrate-from-old + delete-data for portfolios/blogs(+news)/customers + categories; single-select) |
| `/businesses/:businessId/invoices` | Business invoices list |
| `/businesses/:businessId/invoices/new` | Issue invoice (full page) |
| `/users` | Users |
| `/websites` | Websites / domains |
| `/settings` | Platform settings (invoice templates + item templates) |
| `/settings/invoice-templates/new` | Create invoice template |
| `/settings/invoice-templates/:templateId` | Edit invoice template |
| `/invoices/:invoiceId` | Public invoice viewer (no auth; print to PDF) |
| `/profile` | Profile |

### Customer (`apps/customer`)

| Path | Page |
|------|------|
| `/login` | Login (password, register, forgot-password SMS, one-time OTP login) |
| `/checkout` | Shopping cart checkout (standalone layout — login → cart → delivery → payment) |
| `/checkout/login` | Checkout sign-in step |
| `/checkout/cart` | Cart review |
| `/checkout/delivery` | Address or pickup |
| `/checkout/payment` | Discount code + payment |
| `/checkout/success` | Order confirmation |
| `/` | Home |
| `/profile` | Profile |
| `/addresses` | Addresses |
| `/orders` | Orders |
| `/favorites` | Favorites |

### Business (`apps/business`)

| Path | Page | Backend connected? |
|------|------|-------------------|
| `/login` | Login (password, forgot-password SMS, one-time OTP login) | Yes |
| `/` | Home | Partial |
| `/products` | Products hub | — |
| `/products/categories` | Category tree + variations | Yes |
| `/products/brands` | Product brands (linear list) | Yes |
| `/products/list` | My Products grid | Yes |
| `/products/new` | Add product | Yes |
| `/products/edit/:id` | Edit product | Yes |
| `/products/detail/:id` | Product details + gallery | Yes |
| `/products/settings` | Product moderation settings | Yes |
| `/store` | Store hub | Partial |
| `/store/items` | Store items (product variants) | Yes |
| `/store/settings` | Online sell + order process steps | Yes |
| `/customers` | Business customers list | Yes |
| `/store/orders` | Orders list + filters | Yes |
| `/blog` | Blog hub | Yes |
| `/blog/list` | My Blogs grid | Yes |
| `/blog/new`, `/blog/edit/:id` | Add/edit blog | Yes |
| `/blog/categories` | Blog category tree | Yes |
| `/blog/settings` | Comment moderation | Yes |
| `/portfolios` | Portfolios hub | Yes |
| `/portfolios/list` | My Portfolios grid | Yes |
| `/portfolios/detail/:id` | Portfolio detail + gallery | Yes |
| `/portfolios/new`, `/portfolios/edit/:id` | Add/edit portfolio | Yes |
| `/portfolios/categories` | Portfolio category tree | Yes |
| `/portfolios/settings` | Comment moderation | Yes |
| `/website` | Website hub | Placeholder |
| `/website/sliders` | Homepage sliders | Yes |
| `/website/special-categories` | Featured category groups | Yes |
| `/website/special-brands` | Featured brand groups | Yes |
| `/website/special-items` | Special item carousels | Yes |
| `/website/contact` | Contact us submissions list | Yes |
| `/website/subscriptions` | Subscriptions | Placeholder |
| `/website/faq` | FAQ | Placeholder |
| `/website/badges` | Badges | Placeholder |
| `/website/e-payment` | E-payment | Placeholder |

---

## Auth (login pages)

Business and customer login pages share the same SMS-backed flows (super-admin stays password-only).

| Flow | API | Notes |
|------|-----|--------|
| Password login | `POST /auth/login` | Rejects unverified cell when SMS is enabled |
| Register | `POST /auth/register` | Customer by website domain; with SMS on, customer goes to OTP step |
| Send OTP | `POST /auth/send-otp` | Redis 5-min code via Gama |
| One-time login | `POST /auth/login-otp` | Code only → tokens + marks cell verified |
| Forgot password | `POST /auth/reset-password` | Code + new password |
| Verify only | `POST /auth/verify-otp` | Marks verified; no tokens |

Auth helpers: `apps/*/src/services/authService.ts` (`login`, `loginWithOtp`, `resetPassword`, `sendOtp`, `register`). `AuthContext` exposes `login` + `loginWithOtp`.

---

## API surface (business-scoped)

All routes require JWT + business permission. `businessId` comes from tenant context after login.

### Categories
- `GET/POST/PATCH/DELETE /businesses/:businessId/categories`
- `POST /businesses/:businessId/categories/ai-generate` — AI-generated category tree (`{ prompt }`)
- `GET /businesses/:businessId/categories/color-presets`
- `GET/PUT /businesses/:businessId/categories/:categoryId/variations`
- `POST /businesses/:businessId/categories/:categoryId/technical-form/ai-suggest`

Query: `?entityType=product` for product categories.

### Brands
- `GET/POST/PATCH/DELETE /businesses/:businessId/brands`
- `GET /businesses/:businessId/brands/:brandId`

Products accept optional `brandId` on create/update.

### Products
- `GET/POST/PATCH/DELETE /businesses/:businessId/products`
- `POST /businesses/:businessId/products/ai-create` — AI-generated product draft
- `GET /businesses/:businessId/products/:productId`

### Contact submissions
- `GET /businesses/:businessId/contact-submissions` — paginated list
- `GET /businesses/:businessId/contact-submissions/:submissionId`
- `POST /tenants/:host/contact-submissions` — public website contact form

### Business settings
- `GET/PATCH /businesses/:businessId/settings` — includes `branding.primaryColor`

### Portfolios
- `GET/POST/PATCH/DELETE /businesses/:businessId/portfolios`
- `GET /businesses/:businessId/portfolios/:portfolioId`
- Portfolio categories: `?entityType=portfolio`

### Store items (sellable product variants)
- `GET /businesses/:businessId/store-items` — paginated list
- `POST /businesses/:businessId/store-items` — batch create with price + stock

### Store specials
- `GET/POST/PATCH/DELETE /businesses/:businessId/store-specials`
- `GET /tenants/:host/store-specials` — public API for websites

### Website homepage widgets
- `GET/POST/PATCH/DELETE /businesses/:businessId/website/category-groups` — groups with `categoryIds[]`
- `GET/POST/PATCH/DELETE /businesses/:businessId/website/brand-groups` — groups with `brandIds[]`
- `GET /tenants/:host/website/category-groups` — public category groups
- `GET/POST/PATCH/DELETE /businesses/:businessId/website/sliders` — sliders with `slides[]` (imageMediaId, title, linkUrl)
- `GET /tenants/:host/website/sliders` — public sliders

### Customers
- `GET/POST/PATCH/DELETE /businesses/:businessId/customers`
- `GET /businesses/:businessId/customers/search` — autocomplete for order cart

### Orders
- `GET/POST/PATCH/DELETE /businesses/:businessId/orders`

### Product variants & variation values
- `GET/POST/DELETE /businesses/:businessId/products/:productId/variants`
- `GET/PUT /businesses/:businessId/products/:productId/variations`

### Technical forms
- `GET/PUT /businesses/:businessId/categories/:categoryId/technical-form`
- `POST .../technical-form/ai-suggest`
- `GET/PUT /businesses/:businessId/products/:productId/technical-info`

### Media
- `POST /businesses/:businessId/media` — upload to S3

---

## Feature status (business app)

### Connected to backend
- Auth, tenant, business context
- Product categories, variations, products CRUD, media upload
- Product variation values, store items, draft cart, admin orders
- Category technical data forms, product technical data
- Product/store settings, blog, portfolios, contact submissions

### Still local / dummy data
- Product comments modal (`apps/business/src/data/productComments.ts`)
- Legacy seeds in `apps/business/src/data/`

---

## Product variations — business rule

**Product Variations modal:** user selects which category variation **values** apply to the product. Saved via `PUT .../products/:id/variations`.

**Store items:** sellable variants with one value per variation type + price + stock. Saved via `POST .../store-items` (batch).

---

## Database migrations (Backend)

Run in order from `MeshkeeApp Backend/database/migrations/`:

| Migration | Purpose |
|-----------|---------|
| `009_categories_name_fa.sql` | `name_fa` on categories — **required** or categories 500 |
| `010_category_variations.sql` | variation tables |
| `016_product_variation_values.sql` | product variation values |
| `036_invoices.sql` | Platform/business invoices + invoice item templates |
| `037_invoice_name.sql` | Optional invoice name |
| `038_invoice_templates.sql` | Full invoice templates + key points / accounts |
| `039_invoice_account_holder.sql` | Account holder name on bank accounts |
| `040_invoice_public_id.sql` | Opaque 12-digit `public_id` for public links |

After schema changes: `npx prisma generate` and restart the backend.

### Invoices (super-admin)

Two template layers + issued invoices:

| Layer | Purpose |
|-------|---------|
| **Invoice item templates** | Reusable line items (title, duration, worktime, desc, price, discounted) |
| **Invoice templates** | Full blueprints: name, top text, items (from item templates or custom), duplicatable key points, duplicatable bank accounts (bank name, account holder, card, IBAN) |
| **Invoices** | Issued to a business — start from an invoice template (editable) or blank |

| Endpoint | Purpose |
|----------|---------|
| `GET/POST /invoice-item-templates` | Platform line-item presets |
| `PATCH/DELETE /invoice-item-templates/:id` | Update/remove line-item preset |
| `GET/POST /invoice-templates` | Platform full invoice templates |
| `GET/PATCH/DELETE /invoice-templates/:id` | Get / update / delete invoice template |
| `GET/POST /businesses/:businessId/invoices` | List / issue invoices for a business |
| `GET/PATCH/DELETE /businesses/:businessId/invoices/:invoiceId` | Detail, status update, delete |
| `GET /public/invoices/:id` | Public show payload (issued/paid, no auth) |

**Invoice fields:** optional `name`, `topText`, `notes`, `invoiceTemplateId`, `status`, `publicUrl`, nested `items`, `keyPoints`, `accounts` (bank name, account holder, card, IBAN).

**Public invoice viewer (platform):**
- Route: super-admin SPA `/invoices/:id` (`PublicInvoicePage`) — glass layout, print-to-PDF, Approve (`issued` → `approved`), “Issued by” Meshkee footer
- Status `approved`: set from public show page; content becomes immutable for admins
- Edit: list pencil → `/businesses/:businessId/invoices/:invoiceId/edit` (hidden when approved)
- Links use opaque **12-digit `publicId`** (not sequential PK) — `GET /public/invoices/:publicId`
- Local/dev link: current Vite origin (e.g. `https://meshkee.app:5174/invoices/{publicId}`)
- Production link domain: `VITE_INVOICE_PUBLIC_DOMAIN` / `INVOICE_PUBLIC_DOMAIN` (default `meshkee.com`) — optional full origin override via `*_PUBLIC_BASE_URL`
- Until `meshkee.com` proxies or hosts `/invoices/*`, production links may need that DNS/nginx wiring (viewer code ships with super-admin build)

**Migrations:** `036_invoices.sql` … `040_invoice_public_id.sql`

**Super Admin UI:**
- `/settings` — Invoice templates list + item templates (top text preview = one-line ellipsis)
- `/settings/invoice-templates/new` · `/settings/invoice-templates/:id` — full-page template editor (not modal)
- `/businesses/:businessId/invoices` — list + view modal
- `/businesses/:businessId/invoices/new` — full-page issue form
- `/invoices/:id` — public viewer (no auth)
- Schema supports future `owner_scope=business` (business-owned templates)

---

## Key files by area

### Shared packages
| Area | Path |
|------|------|
| API client factory | `packages/dashboard-core/src/api/createApiClient.ts` |
| IRT price utils | `packages/dashboard-core/src/utils/irtPrice.ts` |
| Design tokens | `packages/dashboard-core/src/styles/tokens.css` |
| Toast, Breadcrumbs, etc. | `packages/dashboard-ui/src/` |

### Business app
| Area | Path |
|------|------|
| Categories | `apps/business/src/pages/CategoriesPage.tsx` |
| Products | `apps/business/src/pages/MyProductsPage.tsx`, `AddNewProductPage.tsx` |
| Store / orders | `apps/business/src/pages/StoreItemsPage.tsx`, `OrdersPage.tsx` |
| Services | `apps/business/src/services/` |
| ID helper | `apps/business/src/utils/id.ts` — `createId()` fallback |

### Backend (sibling repo)
| Area | Path |
|------|------|
| Products | `src/products/products.service.ts` |
| Categories | `src/categories/categories.service.ts` |
| Invoices | `src/invoices/` |
| Media / S3 | `src/media/media.service.ts` |
| Prisma schema | `prisma/schema.prisma` |

### Super Admin
| Area | Path |
|------|------|
| Settings / invoice templates | `apps/super-admin/src/pages/SettingsPage.tsx` |
| Template / issue editors | `InvoiceTemplateEditorPage.tsx`, `IssueInvoicePage.tsx` |
| Shared draft fields | `apps/super-admin/src/components/InvoiceDraftFields.tsx` |
| Public invoice viewer | `apps/super-admin/src/pages/PublicInvoicePage.tsx` |
| Business invoices | `apps/super-admin/src/pages/BusinessInvoicesPage.tsx` |
| Invoice API client | `apps/super-admin/src/services/invoiceService.ts` |
| Invoice URL helpers | `apps/super-admin/src/lib/config.ts` |

---

## UI conventions

Match **Super Admin** compact field sizing across all dashboards — see `.cursor/rules/ui-compact-fields.mdc`:

```css
--field-height: 38px;
--field-padding-y: 9px;
--field-font-size: 13px;
```

Global input sizing lives in `packages/dashboard-core/src/styles/tokens.css` (customer) and each app’s `index.css` (business / super-admin). Change password → shared `PasswordResetModal` from `@meshkee/dashboard-ui`.

- **Farsi text:** Customer app → Yekan Bakh (`src/fonts/yekanbakh.css`, `--font-fa` in `index.css`); business/super-admin → IranYekan (`iranyekan.css`, `.faText`). Input + placeholder must share the same stack — see `.cursor/rules/ui-farsi-fonts.mdc`
- **Per-business theme:** Super Admin sets `branding.primaryColor`; business app applies via `BusinessThemeProvider`
- **RTL:** Farsi inputs use `dir="rtl"` and the app’s `--font-fa`
- **IRT prices:** comma-separated thousands + `IRT` suffix — see `.cursor/rules/ui-irt-price.mdc`
- **Toasts:** see `.cursor/rules/ui-toasts.mdc`
- **Icon controls:** see `.cursor/rules/ui-control-buttons.mdc`, `ui-tooltips.mdc`

---

## Known issues & fixes

| Problem | Cause | Fix |
|---------|-------|-----|
| Categories 500 | Missing `name_fa` column | Run migration `009` |
| `crypto.randomUUID is not a function` | Non-secure origin | Use `createId()` from `apps/business/src/utils/id.ts` |
| Media upload 400 | Bad MIME / empty data URL | `ensureJpegUploadFile()`; backend Sharp validation |
| Super Admin HTTPS warning | Missing mkcert certs | Run mkcert per `apps/super-admin/.env.example` |
| Workspace approval popups | Wrong workspace root | Open `MeshkeeApp` monorepo root in Cursor |

---

## Build commands

```bash
npm run build:packages    # shared packages only
npm run build:customer
npm run build:business
npm run build:super-admin
npm run build               # everything
```

---

## Git & handoff checklist

1. Push **MeshkeeApp** monorepo and **MeshkeeApp Backend** to remote
2. Copy `.env` files securely (never commit)
3. Run migrations on the database
4. Open **MeshkeeApp** as Cursor workspace root
5. Start a new agent chat: **"Read `docs/PROJECT_CONTEXT.md` and continue…"**

---

## Branding: logo → favicon

When a business logo is uploaded/changed in **Business Profile**, the backend generates a **48×48 PNG favicon** (sharp), stores it as media, and links `businesses.favicon_media_id`.

| Surface | How favicon is applied |
|---------|------------------------|
| Business dashboard | `TenantBrandingProvider` → `applyDocumentFavicon` |
| Customer dashboard | same |
| Public website (sanihome) | `generateMetadata().icons` from `faviconUrl` |

APIs expose `faviconUrl` (falls back to `logoUrl` if favicon missing):

- `GET /businesses/:id/profile`
- `GET /tenants/:host`
- `GET /tenants/:host/website/business-info`

Migration: `database/migrations/033_business_favicon.sql`

---

## Production deploy (dashboards)

| | |
|--|--|
| VPS | `45.149.76.52` |
| Super Admin | `https://manage.meshkee.com` |
| Business | `https://business.<apex>` (e.g. `business.sanihome.ir`) |
| Customer | `https://customer.<apex>` |
| API | `https://api.meshkee.com/api/v1` (host `185.164.72.119`) |
| Docs | `docs/DEPLOY.md` |

SSL: Certbot cert `meshkee-dashboards` + cron `ssl-sync.sh` every 2h + Super Admin **Sync SSL now** (`POST /domains/ssl-sync` → dashboards agent).

---

## Locale (FA / EN)

Dashboards support **Farsi + English** via shared `LocaleProvider` (`@meshkee/dashboard-ui`) and `LanguageSelect` in headers.

| Piece | Where |
|-------|--------|
| Locale state | `packages/dashboard-ui` → `LocaleContext` |
| Default language per business | `settings.branding.defaultLocale`: `'fa' \| 'en'` (default **`fa`**) |
| Applied on open | Business + customer `TenantBrandingProvider` once from tenant branding |
| Super-admin edit | Businesses list **Lang** column + edit modal |
| Business UI copy | `apps/business/src/i18n/messages.ts` + `useT()` |
| Customer UI copy | `apps/customer/src/i18n/` (parallel pattern) |
| Document titles | Locale-aware via `routeTitles` + `useDashboardDocumentTitle` |
| Fonts | `--font-ui: var(--font-en), var(--font-fa)` — see `.cursor/rules/ui-farsi-fonts.mdc` |

RTL: `dir="rtl"` when locale is `fa`.

---

## Business home dashboard

Home is **not** a 1:1 mirror of the sidebar. Tiles are curated (no Settings tile).

| Feature | Detail |
|---------|--------|
| Section cards | Count pill + arrow (settings/website have arrow only); hover fills primary |
| Counts | Existing list `total` with `pageSize=1` — light, not a new stats join |
| Charts (6/12 each) | Orders (+ add-to-basket) and Customers (+ active logins), last 30 days |
| Chart API | `GET .../orders/activity?days=30`, `GET .../customers/activity?days=30` |
| Chart colors | Primary series = theme primary; accent = purple for red theme, cyan for blue themes (`--chart-accent` from `businessPrimaryColors`) |
| Page aura | Slow-moving radial blobs on `body::before` (tokens + business `index.css`) |

Products overview page uses the same i18n + theme-aware `ProductActivityChart` (added vs updated, 12 months).

---

## Suggested next work

- Point `meshkee.com/invoices/*` at the public invoice viewer (proxy or dedicated host)
- Business-dashboard invoice templates + issue flow (`owner_scope=business`)
- Migrate business and super-admin fully onto `@meshkee/dashboard-core` / `@meshkee/dashboard-ui` (LocaleProvider already shared)
- Finish FA/EN coverage on remaining business form pages (many labels still English)
- Connect product comments to backend
- Wire order status transitions to store `orderProcessSteps`
- Enforce `onlineSellEnabled` on public website checkout
- Postman collection updates for variants endpoints
- Add `sanihome.ir` (and other tenants) in production Super Admin so tenant APIs resolve

---

## Agent instructions

1. **Minimize scope** — only change what the task requires
2. **Match existing patterns** — services in `src/services/`, CSS modules, NestJS modules per domain
3. **Do not commit** unless the user explicitly asks
4. **Test user:** `+989122222222` / `password`; business id **4** for sanihome.ir
5. **Restart backend** after Prisma or module changes
6. Update **this file** when completing a major feature
