# 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: September 8, 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, OPENAI_API_KEY (or GROQ_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). **Add/Edit domain** accepts optional Git repo URL → provisions storefront on websites VM and sets `deploy_slug`. Optional **Update DNS records** checkbox upserts tenant DNS (`@` → `ns.meshkee.com`, `www` CNAME → apex, `business`/`customer`/`api` CNAMEs) via **Arvan** or **Cloudflare** (switch; Cloudflare is DNS-only / not proxied). | | `/businesses/:businessId/invoices` | Business invoices list | | `/businesses/:businessId/invoices/new` | Issue invoice (full page) | | `/users` | Users (business filter: Customer vs Manager → Admin/Editor/Viewer; Admin assignable by super-admin only; owners locked) | | `/websites` | Websites / domains (Deploy when `deploy_slug` set). Edit domain can optionally update DNS (Arvan or Cloudflare, same tenant records as Businesses). Overflow ⋯ includes website/dashboard links, **Email DNS** (Stalwart DKIM + MX/SPF on Arvan or Cloudflare; removes extra MX, `dkim._domainkey`, `_dmarc`), **AI prompts**, and remove. **Parked domains** button: alias hosts 301 to the main domain. Per alias, choose **Arvan** or **Cloudflare** for DNS (`@` + `www` only, not proxied); SSL is always Let's Encrypt on the websites VM. | | `/templates` | Global AI prompt templates. Assign from Businesses or Websites ⋯ → **AI prompts** (add from library; public pack link). | | `/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 (online → bank redirect) | | `/checkout/result` | Bank return callback (`status`, `orderId`, payment meta) | | `/checkout/success` | Order confirmation (+ payment data when from gateway) | | `/checkout/failed` | Checkout / payment failure (+ payment data when present) | | `/` | Home | | `/profile` | Profile | | `/addresses` | Addresses | | `/orders` | Orders | | `/favorites` | Favorites | | `/my-products` | Customer stock listings (gated by `customer_products`) — list API | | `/my-products/:id` | User product details | | `/my-products/new` | Add user product (3 steps: basics, images, technical) — create API | | `/my-products/:id/edit` | Edit user product — update API | ### 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 | | `/finance` | Finance hub (Invoices + Transactions tiles) | Yes | | `/invoices` | Business-issued invoices list (`?userId=` filter) | Yes | | `/invoices/new` | Issue invoice to a user | Yes | | `/invoices/:invoiceId/edit` | Edit invoice (locked when approved) | Yes | | `/invoices/templates` | Invoice templates + item templates | Yes | | `/invoices/templates/new` | Create invoice template | Yes | | `/invoices/templates/:templateId` | Edit invoice template | Yes | | `/customer-products` | Customer user-product listings (admin API) | Yes | | `/customer-products/new` | Admin create user product (under admin name) | Yes | | `/customer-products/:id` | Customer user-product details | Yes | | `/customer-products/:id/edit` | Admin edit user product | 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/static-images` | Named website image slots, grouped by page | Yes | | `/website/special-items` | Special item carousels | Yes | | `/website/settings` | Website settings (special products source: product vs store item) | Yes | | `/website/contact` | Contact us submissions list | Yes | | `/website/subscriptions` | Subscriptions | Placeholder | | `/website/faq` | FAQ | Placeholder | | `/website/badges` | Badges | Placeholder | | `/settings` | General settings | E-payment: Mellat + ZarinPal credentials; stubs for SEP / Snapp Pay / DigiPay | --- ## 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 | | Dashboard SSO | `POST /auth/handoff` + `POST /auth/handoff/consume` | Admin icon on customer dashboard; 60s one-time ticket | Auth helpers: `apps/*/src/services/authService.ts` (`login`, `loginWithOtp`, `resetPassword`, `sendOtp`, `register`, `createHandoff`). `AuthContext` exposes `login` + `loginWithOtp`. Business dashboard active tenant is resolved from the host domain (never `businesses[0]` alone). Customer header “open business dashboard” uses a one-time SSO ticket so staff do not log in again. --- ## 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` - Translations (EN/AR, stored in `entity_translations`): `GET .../products/:id/translations`, `POST .../translations/:locale/ai`, `PUT .../translations/:locale`. Same three routes on categories. Dashboard category list includes `translationCount`. Bulk: `POST .../categories/translations/ai-all` and `POST .../products/translations/ai-all` (missing EN/AR only; skips existing). Business dashboard shows translation controls only when the `multilanguage_data` module is enabled for that business. ### 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` — `branding`, `dashboard`, `store`, `modules`, `website` - `website.specialProductsSource`: `product` \| `store_item`. Unset → `store_item` if the store module is enabled, otherwise `product`. - **Torob:** store-module websites with Store settings → Torob on. Nginx on the shop apex proxies `POST /torob_api/v3/products` to `POST /api/v1/tenants/{domain}/torob_api/v3/products`. Otherwise 404. Do not implement this in Next.js. - `modules.enabled`: optional modules — business: `products`, `store`, `portfolio`, `blog`, `warehouse`, `videos`, `finance`; customer: `customer_products`. Missing key → all business modules enabled (legacy); customer modules stay opt-in. Super-admin only for modules PATCH. - `modules.charts`: two home chart slots (`none`, `orders_30d`, `customers_joined_1y`, `blog_views_30d`, `products_added_1y`, `product_views_30d`, `website_views_30d`, `portfolio_views_30d`); `none` hides that slot; defaults orders + customers. View charts read `website_page_views` (auto-counted on public list/detail GETs; ~6 month retention). `website_views_30d` aggregates **all** page kinds (`kind=all`); product/portfolio/blog charts filter by detail kind. Home-only hits also increment on `GET .../website/sliders` and `GET .../website/static-images?pageKey=home`. Lifetime totals in `website_page_view_counters`. - **Statistics** (`/statistics`, always-on sidebar): same two home chart slots, then visit KPI rows (today / this week Sat–Fri / this month / lifetime total) for all pages, home, products (+ store items), blog, portfolio, videos — module-gated except total + home. API: `GET businesses/:id/analytics/views/summary`. - Always-on (not in modules list): business — home, **statistics**, customers, website, profile, settings; customer — home, profile, addresses, orders, favorites - Tenant public: `GET /tenants/:host` includes `enabledModules` + `homeCharts` + `specialProductsSource` ### 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 - `GET /businesses/:businessId/store-items/export` — Excel of every variant (id, names, variation, price, stock) - `POST /businesses/:businessId/store-items/import` — upload that Excel to bulk-update 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/static-images` — named image slots (`single` or `list`) with `pageKey` + per-image `titleFa` / `titleEn` / `subtext` / `linkUrl` - `POST /businesses/:businessId/website/static-images/sync` — read slot keys from the live website (`GET https://{domain}/meshkee/static-image-slots`) and upsert them - `GET /tenants/:host/website/static-images` — public slots (`?pageKey=home` to filter by page) - `GET /tenants/:host/website/static-images/:key` — one public slot - `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 - `GET /businesses/:businessId/customers/export` — Excel (.xlsx) of business customers (`phone`, `first_name`, `last_name`) - `POST /businesses/:businessId/customers/import` — Excel import for SMS lists; only `phone` required; new users get a random password hash (OTP login still works); existing users are linked; already-linked / staff rows skipped - `POST /businesses/:businessId/customers/:userId/sms` — single SMS via Gama **SendQuick**; body `{ message, templateId? }` - `POST /businesses/:businessId/customers/sms` — bulk SMS via Gama **SendBulk** (or per-recipient when template/vars); body `{ message, userIds?, templateId? }` — empty/omitted `userIds` → all enabled customers - Free-text SMS source: `settings.sms.senderNumber` (Super Admin only) or advertising shortcode `9000590009` - Verified SMS template → service shortcode `5000110005` (fixed); unverified template → `9000590009` - Per-business prepaid `businesses.sms_balance` (IRT) — all Meshkee SMS (OTP login/forgot-password, customer SMS, partner/public SMS for linked domains) costs **400 IRT per 64 characters** per recipient (`ceil(len/64)*400`). Insufficient balance blocks send. - New businesses created in Super Admin start with **500,000 IRT** SMS balance - External domains (no Meshkee website) still bill via their business row once the domain is linked - Business Settings → **SMS settings**: balance (read-only) + template CRUD - Super Admin business overflow → **SMS settings** modal: sender number + SMS balance top-up - Super Admin → **SMS Templates** (`/sms-templates`): verify / unverify - Online payment gateways live under **Website → Settings** (not main Settings) - Template vars: `{{user.name}}`, `{{user.lastname}}`, `{{user.title}}` ### 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 ### User products (customer listings) - Customer (own stock): `GET/POST /businesses/:businessId/my-user-products`, `GET/PATCH/DELETE .../:productId`, `POST .../:productId/promote`, categories + technical-form + media - Business admin (all listings): `GET/POST /businesses/:businessId/user-products`, `GET/PATCH/DELETE .../:productId`, `PATCH .../:productId/status` (`draft` | `published` | `rejected` | `archived`), `POST .../:productId/promote`, categories + technical-form. Admin create is attributed to the admin user. --- ## 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 | | `041_invoice_status_approved.sql` | Invoice status `approved` | | `058_invoice_user_id.sql` | `invoices.user_id` billed user | After schema changes: `npx prisma generate` and restart the backend. ### Invoices (super-admin + business) Billed party is always a **User** (`user_id`). `business_id` is tenant/context. Platform invoices: `owner_scope=platform` (super-admin). Business-issued: `owner_scope=business` + `issuer_business_id`. | Layer | Purpose | |-------|---------| | **Invoice item templates** | Reusable line items (title, duration, worktime, desc, price, discounted) | | **Invoice templates** | Full blueprints: name, top text, items, key points, bank accounts | | **Invoices** | Issued **to a user** — from template or blank | | Endpoint | Purpose | |----------|---------| | `GET/POST /invoice-item-templates` | Platform line-item presets (super-admin) | | `PATCH/DELETE /invoice-item-templates/:id` | Update/remove platform preset | | `GET/POST /invoice-templates` | Platform full invoice templates | | `GET/PATCH/DELETE /invoice-templates/:id` | Platform template CRUD | | `GET/POST /businesses/:businessId/invoice-item-templates` | Business item templates | | `PATCH/DELETE .../invoice-item-templates/:id` | Business item template CRUD | | `GET/POST /businesses/:businessId/invoice-templates` | Business invoice templates | | `GET/PATCH/DELETE .../invoice-templates/:id` | Business invoice template CRUD | | `GET/POST /businesses/:businessId/invoices` | List / issue (`?userId=` filter; body `userId` required for business) | | `GET/PUT/PATCH/DELETE .../invoices/:invoiceId` | Detail, content, status, delete | | `GET /public/invoices/:publicId` | Public show (platform or business; issued/approved/paid) | | `POST /public/invoices/:publicId/approve` | Public approve (`issued` → `approved`) | Auth: platform template routes → super-admin. Business routes → `BusinessPermissionGuard` (`invoices.*` / `invoice_templates.*`); super-admin on invoice paths still sees **platform** invoices for that business. **Invoice fields:** `userId` + nested `user`, optional `name`, `topText`, `notes`, `invoiceTemplateId`, `status`, `publicUrl`, nested `items`, `keyPoints`, `accounts`. **Public invoice viewer:** - Super-admin SPA `/invoices/:publicId` (`PublicInvoicePage`) — print-to-PDF; opaque 12-digit `publicId` - Payload includes `locale` (`fa` | `en` from business `settings.branding.defaultLocale`) — page is RTL + Farsi chrome when `fa` - Platform invoices: `https://{INVOICE_PUBLIC_DOMAIN}/invoices/{publicId}` (default `meshkee.com`) - Business invoices: `https://{primaryBusinessDomain}/invoices/{publicId}` (e.g. `sanihome.ir`) **Migrations:** `036_invoices.sql` … `041_invoice_status_approved.sql`, `058_invoice_user_id.sql` **Super Admin UI:** `/settings` templates; `/businesses/:id/invoices` list/issue/edit; public `/invoices/:publicId` **Business UI:** sidebar Finance group → `/finance` (hub), `/invoices`, `/transactions`; home Finance tile; Users row Receipt icon → `/invoices?userId=`; i18n en/fa via `useT` --- ## 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`); business/super-admin → IranYekan (`iranyekan.css`). `--font-en` / `--font-fa` are named faces only; `--font-ui` appends system fallbacks. Self-host files in `public/fonts/` — see `.cursor/rules/ui-farsi-fonts.mdc` - **Per-business theme:** Super Admin sets `branding.primaryColor` and `branding.themeMode` (`light` \| `dark`); business + customer dashboards apply both via their theme providers (`data-theme` on ``). Dark mode uses neutral gray surfaces; `--primary` stays for accents only. - **Per-business modules:** Super Admin **Modules** column on Businesses list; stored in `settings.modules.enabled` (business + customer sections); business Home + Sidebar hide disabled business modules; `customer_products` gates customer My Products (`/my-products`, `/my-products/new`) which call `businesses/:id/my-user-products` - **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.` (e.g. `business.sanihome.ir`) | | Customer | `https://customer.` | | 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 dashboard SSL** (`POST /domains/ssl-sync` → dashboards agent). Storefront SSL: **Issue website SSL** (`POST /domains/website-ssl`) → websites agent `/ssl`. Per-row lock: **Ensure SSL** (`POST /domains/:id/issue-ssl`) probes apex + `business.*` + `customer.*`, issues only what’s missing (websites agent for apex when `deploy_slug` set; blocking dashboard ssl-sync when business/customer need certs). --- ## 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) - 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 - Customer dashboard “my invoices” view --- ## 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