# ModuleDesk — Complete App Facts, Feature Spec & FAQ

> Source-verified against codebase (git e617f1e, 2026-05). Each claim includes file:line.
> Use for marketing copy, sales page content, onboarding, and internal reference.

---

## What it is (Elevator)

ModuleDesk is a multi-tenant SaaS support desk purpose-built for **sellers on the PrestaShop Addons Marketplace**. It pulls every Addons message and order into one inbox, layers an AI co-pilot that classifies, drafts, proofreads, translates and audits replies, and adds tooling that the Addons back-office lacks: a per-customer credential vault with live FTP/SFTP testing, automatic website health and module-presence checks, semantic search and topic segmentation across the full ticket history, generated documentation guides clustered from real resolved tickets, and end-to-end revenue/customer analytics. The whole stack runs on PostgreSQL with **schema-per-tenant isolation** and an in-memory numpy RAG index — no external vector database.

---

## Identity & Stack

| Field | Value | Source |
|-------|-------|--------|
| Product brand | ModuleDesk | CLAUDE.md memory; rebrand commits ee3047d, 8ba5976 |
| Package name | `supporthub` | repo root |
| Language / runtime | Python 3.10+ | venv/bin/python |
| Web framework | Flask 3.0.0 | requirements.txt:2 |
| ORM / migrations | SQLAlchemy 2.0.21 + Alembic 1.12.1 | requirements.txt:7-8 |
| Database | PostgreSQL (psycopg2-binary ≥2.9.9) | requirements.txt:9 |
| Auth | Flask-Login 0.6.3, Flask-WTF 1.2.1 (CSRF), bcrypt 4.0.1 | requirements.txt:3-4,10 |
| Email | Flask-Mail ≥0.9.1 (SMTP) | requirements.txt:5 |
| Encryption | `cryptography` (Fernet) | requirements.txt:11 |
| HTML sanitization | bleach 6.1.0 | requirements.txt:12 |
| HTTP client | httpx ≥0.27.2 | requirements.txt:13 |
| AI SDK | openai ≥1.11.0 | requirements.txt:14 |
| Background jobs | Celery ≥5.3.0 (Redis ≥5.0.0 broker) + in-process daemon threads | requirements.txt:21-22 |
| Doc scraping | crawl4ai ≥0.4.0 (headless Chromium) | requirements.txt:19 |
| Billing | Stripe ≥7.0.0 | requirements.txt:23 |
| FTP testing | paramiko ≥3.4.0 + stdlib ftplib | requirements.txt:24 |
| Vector index | numpy 1.26.4 in-memory float32 | requirements.txt:18; embedding_service.py:56-82 |
| Deployment | Phusion Passenger on cPanel/CloudLinux; dev via Flask --debug + systemd | DEPLOY.md; CLAUDE.md |
| Reverse proxy | Caddy → `support.claude-baby.dev` | memory/caddy-setup.md |
| Sessions | 30-day permanent cookie `supporthub_session` | main.py:297-300 |
| Migration count | 33 Alembic revisions | supporthub/migrations/versions/ |

---

## User-Facing Feature List

What a user can actually *do* inside ModuleDesk, in plain language. (Implementation details for each are in the sections further down.)

### Inbox & Tickets
- Unified inbox of every customer message across all your Addons modules.
- Filter by status, priority, tag, product, free-text search, pre-sale only.
- Sort by **Smart Priority** (multi-signal scoring), most recent, oldest, or hottest unread.
- Open a ticket and see the full conversation, customer profile, order history, support status, module version, language, and a TL;DR for long threads.
- Mark tickets read / unread, starred, closed, reopened.
- Set priority (Critical / High / Normal / Low) and add free-form tags.
- Save a Quill rich-text draft per ticket that auto-restores when you come back.
- Click "Refresh" to re-pull only that one thread from Addons without leaving the page.
- "Next Ticket" popup after sending a reply recommends what to work on next (Quickest or Highest-Priority mode).

### AI Reply Composer
- One-click **Write Reply** generates a full draft tailored to the latest customer message.
- **Improve** rewrites your existing draft with optional instructions.
- **Proofread** fixes grammar and clarity without changing the message.
- **Shorten** condenses verbose drafts.
- **Enrich** pulls facts from similar past tickets and your scraped docs.
- **Check Reply** audits whether your draft actually answers every question in the inbound message.
- After sending, the reply's status is auto-detected (waiting for customer / answered) and confirmed via popup when ambiguous.
- Thumbs up / down / "used as is" / "edited" feedback on each AI suggestion shapes future recommendations.

### Saved Replies (Predefined Messages)
- Quick-reply dropdown grouped by category right in the ticket editor.
- Three sources: local, synced from Addons, or saved from an AI suggestion.
- Global or product-specific templates.
- Auto-attach the latest module ZIP to a reply when you use the template.
- `[Review]`-prefixed templates auto-selected by customer language for review requests.
- Admin can create, edit, delete, recategorise templates from the Predefined Messages page.

### Translation & Languages
- Auto-detect customer language on incoming messages.
- One-click translate any message into your preferred language (cached).
- Translate your draft outbound into the customer's language before sending.
- Set known languages and preferred display language in Settings.

### Attachments
- Drag-and-drop multiple files to a reply.
- Auto-zipped server-side when 2+ files (named `attachments.zip`).
- Up to 10 MB per file, 50 MB total.
- "Attach Latest Module" button to send the most recent module ZIP in one click.
- Attachments behind Cloudflare are fetched transparently (no broken downloads).
- View any attachment in-app with a lightbox and copy text/image to clipboard.

### Topic Segmentation & TL;DR
- Long threads are automatically split into topic segments with AI-generated labels.
- Sticky in-thread headers and a sidebar mini-nav let you jump between segments.
- Threads with 5+ messages show a TL;DR banner at the top.

### Search
- Semantic search across every ticket you've ever received (cosine similarity, no keyword required).
- Combined with keyword search for exact-phrase matches.
- Filter results by product or status.

### Documentation Guides
- "Possible Guides" amber badge on each module on the Products page shows recurring topics that aren't documented yet.
- Click ⚡ on any module to see the candidate clusters with representative tickets.
- One-click "Generate Guide" turns a cluster into a Docusaurus MDX article.
- Generated guides listed at `/guides` with markdown content ready to copy into your docs site.
- Add external doc sources (your help site, public docs) — they're scraped and indexed so the AI uses them when drafting replies.

### Customer Profiles
- Per-customer page with: orders, total revenue, support history, threads, language, country, website health, installed modules.
- Customer list with filters for country, customer type, Business Care, online/offline site, module installed.
- Rebuild customer profiles after CSV imports.

### Orders
- Full order list with date / product / module-version / margin / refund filters.
- Sync recent (3 months), historical (up to 120 months), or full 10-year backfill.
- Import the Addons orders CSV (dedup against API-synced rows).
- Export filtered orders or the dashboard date-range to CSV.

### Module Catalog
- Module-level page with: support load, ratings, current version, compatibility range, BC link, last update, days since update.
- Upload versioned module ZIPs (visible in ticket composer "Attach Latest Module").
- Set the module slug used for customer-site module-presence detection.
- Per-module: written guides count + possible guides count badge.

### Credential Vault (per customer)
- Group multiple sites per customer; multiple credentials per site.
- Types: Back Office / FTP / FTPS / SFTP / htaccess / cPanel / SSH / Other.
- All passwords encrypted at rest (Fernet); decrypted only on click.
- Optional label per credential (hidden behind "+ Add a label" toggle to keep the form clean).
- **Test FTP/FTPS/SFTP Connection**: one-click live test that connects, authenticates, and lists the home folder. Auto-runs after save.
- Copy-Connection shortcut (`sftp user@host:port`) to clipboard.
- "Quick Login" on BO credentials opens the back-office URL and copies the password.
- Every password reveal and connection test is audit-logged.

### Customer Website Health
- Automatic background checks of every customer's website (online / offline / module installed).
- Colored status indicators in inbox, customer list, order list, customer detail, ticket sidebar.
- One-click full rescan or fast module-only rescan.
- Module presence detected by probing your module's static asset path.

### URL Safety
- Every customer-supplied URL gets a VirusTotal-cached caution badge (safe / warning / dangerous).
- Unvalidated URLs styled with an "open with caution" indicator.

### Dashboard & Analytics
- KPI tiles: total / unread / open / closed tickets, average first-response time.
- Revenue and orders-over-time charts with a date-range picker.
- Customer geography map.
- Product performance breakdown.
- Customer value segments.
- Period ticket stats for any date range.
- Export dashboard date range to CSV.

### Sync
- Auto-sync every 30 minutes (Addons threads, products, predefined messages).
- Manual "Sync Now" button on every page.
- Historical sync with **pause and resume** support — start a backfill, walk away, come back later.
- Toast / growl notifications with new thread + message counts after each sync.

### Settings
- Light / dark / auto theme (persisted).
- Reply signature (HTML).
- Auto-close inactive tickets after N days.
- Known and preferred translation languages.
- Doc sources: add URLs, scrape on demand or all-at-once, see per-source page counts.
- Team management: invite members by email (token link), change roles (admin / agent), remove members.

### Onboarding (new orgs)
- Email + password signup, email verification.
- Walks through: connect Addons API key → confirm modules → pick plan → choose AI mode (platform / BYOK) → optional Stripe checkout → tenant schema provisioned automatically.
- Team-member invite acceptance flow.

### Roles & Permissions
- **Agent**: read tickets, reply, set status/priority/tags.
- **Admin**: everything above plus settings, templates, doc sources, module uploads, sync controls, team management.
- **Platform Admin**: cross-org console — list orgs, suspend, change plans, view audit.

### Billing (Stripe)
- Per-module recurring price model (Basic / Pro).
- Subscription quantity mirrors your active module count.
- Free plan: 2 modules, 20 AI credits/month, 5 templates.
- Pro plan: unlimited + 1,000 AI credits/month, or use your own OpenAI key (BYOK) for unlimited.
- Free-tier credits reset on the 1st of every month automatically.

### Audit Log
- Every reply, status change, tag change, read/unread flip, AI suggestion, credential reveal, and FTP test is logged per tenant with user, IP, and timestamp.
- Platform-level admin actions (org suspend / plan change) logged in a separate public audit log.

### Mobile / Theming
- Tailwind-based responsive layout, mobile sidebar toggle.
- Dark mode auto-follows system theme by default.
- Sticky toolbars, slide-out AI suggestion panel on small screens.

---

## Codebase Size

| Path | Lines | Purpose |
|------|------:|---------|
| `supporthub/app/main.py` | 5,055 | All Flask routes, view functions, helpers, sidebar cache, startup logic |
| `supporthub/app/services/sync_service.py` | ~1,700 | Addons API sync (incremental + historical), products, predefined messages, auto-close |
| `supporthub/app/services/embedding_service.py` | ~1,100 | EmbeddingIndex (in-memory RAG), search, guide candidates, doc sub-index |
| `supporthub/app/services/ai_service.py` | ~1,100 | OpenAI client, classify+draft, improve/proofread/shorten/enrich/check, translate, guide generation, BYOK |
| `supporthub/app/services/order_sync_service.py` | ~900 | Order API sync, CSV import, customer profile rebuild, dedup |
| `supporthub/app/services/website_check_service.py` | ~500 | Customer site health + module-presence checks |
| `supporthub/app/services/segmentation_service.py` | ~400 | Topic segmentation (TextTiling + time gaps + AI labeling) |
| `supporthub/app/services/doc_scraper_service.py` | ~380 | Sitemap-driven doc scraping, crawl4ai + httpx fallback |
| `supporthub/app/services/ftp_test_service.py` | 198 | FTP/FTPS/SFTP test (connect + login + list home) |
| `supporthub/app/services/{billing,credential,plan,dashboard_stats,segmentation,tenant,ticket,auth,url_safety}_service.py` | ~3,300 (combined) | Stripe, Fernet vault, plans, analytics, segmentation, tenant bootstrap, VirusTotal |
| `supporthub/app/models.py` | 668 | All tenant-schema SQLAlchemy models |
| `supporthub/app/static/app.js` | 3,893 | Full SPA-ish frontend: AI composer, credentials UI, search, attachments, sync polling, toasts |
| `supporthub/app/templates/ticket.html` | ~3,100 | Ticket detail page (biggest single template) |
| `supporthub/app/templates/products.html` + `customers.html` | ~1,800 | Module and customer list pages |
| Total | **~9,700+** | Excludes alembic, tests, JS comments |

---

## Routes Inventory

All non-billing routes require `@login_required` unless noted. `@require_role('admin'|'platform_admin')` guards destructive/admin endpoints.

### Authentication & Onboarding

| Method | Path | View | Purpose |
|--------|------|------|---------|
| GET/POST | `/login` | `login` | Email+password, sets 30-day permanent session (main.py:531) |
| GET | `/logout` | `logout` | Clear session (main.py:550) |
| GET | `/` | `index` | Redirect to /dashboard (main.py:556) |
| GET/POST | `/register` | onboarding | Create org + first admin (onboarding/routes.py) |
| GET | `/verify-email/<token>` | onboarding | Email verification link |
| GET/POST | `/onboard/{api-key,modules,plan,ai,billing}` | onboarding | 5-step provisioning wizard |
| GET | `/onboard/complete` | onboarding | Provision tenant schema + finish |
| GET/POST | `/accept-invite/<token>` | onboarding | Team member invite (onboarding/routes.py:449) |

### Inbox & Tickets

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/inbox` | Paginated 25/page, filters: status, priority, tag, search, product, presale; sort: `smart_priority` etc. (main.py:562) |
| GET / POST | `/ticket/<id>` | Detail + status/priority/tags/reply/AI/translate (main.py:921, 1146) |
| POST | `/ticket/<id>/star` | Toggle starred (main.py:846) |
| GET / POST | `/ticket/<id>/draft` | Get/save Quill draft HTML (main.py:858, 867) |
| POST | `/ticket/<id>/refresh` | Refetch from Addons without page reload (main.py:1469) |
| GET | `/attachment/<id>` | Serve attachment (cache + Playwright fallback for Cloudflare) (main.py:1612) |
| POST | `/sync` | Trigger incremental sync (main.py:819) |

### AI Composer

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/api/ai/write-reply` | Full classification + draft (main.py:3119) |
| POST | `/api/ai/improve-draft` | Improve existing draft (main.py:3154) |
| POST | `/api/ai/proofread` | Proofread (main.py:3170) |
| POST | `/api/ai/shorten` | Shorten (main.py:3185) |
| POST | `/api/ai/enrich` | Add RAG-sourced facts (main.py:3200) |
| POST | `/api/ai/check-reply` | Coverage QA — does draft answer all questions? (main.py:3219) |
| POST | `/api/suggestion/<id>/feedback` | Thumbs up/down/used_as_is/edited (main.py:3236) |
| POST | `/api/suggestion/<id>/save-as-template` | Save AI draft as PredefinedMessage [admin] (main.py:3398) |
| POST | `/api/suggestion/<id>/generate-guide` | Generate Docusaurus MDX [admin] (main.py:3452) |
| POST | `/api/message/<id>/translate` | Translate message (cached) (main.py:2888) |
| POST | `/api/translate-draft` | Translate draft (uncached) (main.py:2954) |

### Search

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/search` | Semantic search page (main.py:881) |
| POST | `/api/search` | Combined semantic + keyword (threshold 0.40, top_k 25) (main.py:895) |

### Predefined Messages

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/predefined-messages` | List + manage (main.py:1673) |
| POST | `/predefined-messages/create` | Create [admin] (main.py:1722) |
| POST | `/predefined-messages/<id>/edit` | Edit [admin] (main.py:1773) |
| POST | `/predefined-messages/<id>/delete` | Delete [admin] (main.py:1822) |
| GET | `/api/predefined-messages/for-product/<id>` | Quick-reply JSON (main.py:1895) |
| GET/POST | `/api/predefined-message-categories[/<id>[/delete]]` | Category CRUD (main.py:1835-1884) |
| GET | `/api/review-request-template` | Auto-find [Review]-prefixed template by language (main.py:2081) |

### Products

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/products` | Modules + load + guides + candidate badge (main.py:3526) |
| POST | `/api/products/<id>/upload-release` | Upload module ZIP [admin] (main.py:1944) |
| GET | `/api/products/<id>/releases` | List releases (main.py:2003) |
| POST | `/api/products/<id>/releases/<rid>/delete` | Delete release [admin] (main.py:2029) |
| GET | `/api/products/<id>/download-latest` | Stream latest ZIP (main.py:2062) |
| POST | `/api/products/<id>/slug` | Set module_slug [admin] (main.py:4259) |
| GET | `/api/products/<id>/guide-candidates` | Cluster strong threads → guide candidates (main.py:3672) |
| POST | `/api/products/generate-guide` | Generate guide from thread summary [admin] (main.py:3684) |
| GET | `/api/products` | JSON list for dropdowns (main.py:2130) |

### Customers & Orders

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/customers` | Paginated, filter by search/country/ctype/bc/online/module (main.py:4471) |
| GET | `/customers/<hash>` | Full profile: orders, threads, support, site check (main.py:4742) |
| POST | `/api/customers/rebuild` | Rebuild all customer profiles [admin] (main.py:4457) |
| GET | `/orders` | Order table with filters (main.py:3723) |
| POST | `/api/orders/sync` | Sync last 3 months (main.py:3910) |
| POST | `/api/orders/sync-historical` | Up to 120 months [admin] (main.py:3932) |
| POST | `/api/orders/sync-all` | Full 10-year backfill [admin] (main.py:3955) |
| POST | `/api/orders/dedup` | Dedup API↔CSV [admin] (main.py:3976) |
| GET | `/api/orders/export-csv` | Filtered CSV export (main.py:3999) |
| POST | `/api/orders/import-csv` | Background CSV import (main.py:4129) |
| GET | `/api/orders/import-csv/progress/<task_id>` | Poll progress (main.py:4240) |

### Credential Vault

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/credentials/<hash>` | List sites + creds (passwords masked) (main.py:2328) |
| POST | `/api/credentials/<hash>/sites` | Add site (main.py:2337) |
| PUT / DELETE | `/api/credentials/sites/<id>` | Edit/delete site (main.py:2352-2367) |
| POST | `/api/credentials/sites/<id>/creds` | Add credential (main.py:2378) |
| PUT / DELETE | `/api/credentials/creds/<id>` | Edit/delete credential (main.py:2398-2418) |
| GET | `/api/credentials/creds/<id>/reveal` | Decrypt password — **audit-logged** (main.py:2429) |
| POST | `/api/credentials/creds/<id>/test` | Live FTP/FTPS/SFTP test — **audit-logged** (main.py:2447) |

### Documentation Sources

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/settings/doc-sources/add` | Add doc source [admin] (main.py:3277) |
| POST | `/settings/doc-sources/<id>/delete` | Delete [admin] (main.py:3312) |
| POST | `/api/doc-sources/<id>/scrape` | Scrape one (background) [admin] (main.py:3329) |
| POST | `/api/doc-sources/scrape-all` | Scrape all (background) [admin] (main.py:3353) |
| GET | `/api/doc-sources/status` | Page count + last_scraped per source (main.py:3381) |
| GET | `/guides` | List generated guides (main.py:3487) |
| GET | `/api/guides/<id>` | Guide JSON (main.py:3510) |

### Sync & Website Checks

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/start-historical-sync` | Full backfill [admin] (main.py:3019) |
| POST | `/pause-historical-sync` | Pause [admin] (main.py:3058) |
| POST | `/resume-historical-sync` | Resume [admin] (main.py:3077) |
| GET | `/api/sync-status` | Poll progress (main.py:2985) |
| POST | `/api/website-checks/run` | Full batch [admin] (main.py:4272) |
| POST | `/api/website-checks/stop` | Stop running [admin] (main.py:4366) |
| POST | `/api/website-checks/rescan-modules` | Fast module-only rescan [admin] (main.py:4376) |
| GET | `/api/website-checks/status` | Summary (main.py:4441) |
| GET | `/api/website-checks/progress` | Live progress (main.py:4449) |
| GET | `/api/check-url` | VirusTotal scan (cached 7d) (main.py:2314) |

### Settings & Sidebar

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/settings` | Settings page [admin] (main.py:2612) |
| POST | `/settings/auto-close` | Auto-close days [admin] (main.py:2680) |
| POST | `/settings/languages` | Known/preferred languages [admin] (main.py:2711) |
| POST | `/settings/signature` | Reply signature [admin] (main.py:2746) |
| POST | `/settings/theme` | Light/dark/auto theme (main.py:2767) |
| GET | `/settings/team` | Team mgmt [admin] (main.py:2786) |
| POST | `/settings/team/members/<id>/role` | Change role [admin] (main.py:2812) |
| POST | `/settings/team/members/<id>/remove` | Remove member [admin] (main.py:2832) |
| POST | `/settings/team/invite` | Send invite email [admin] (main.py:2848) |
| GET | `/dashboard` | Stats + charts + date range (main.py:2478) |
| GET | `/api/sidebar-counts` | Live badge counts (30s cached) (main.py:2142) |
| GET | `/api/next-ticket` | Suggest next ticket post-reply (main.py:2199) |

### Platform Admin (blueprint `/admin`, `platform_admin` only)

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/admin/` | List all orgs (admin/routes.py:29) |
| GET | `/admin/org/<id>` | Org detail + AI usage + audit (admin/routes.py:58) |
| POST | `/admin/org/<id>/suspend` / `/unsuspend` | Suspend/restore (admin/routes.py:105-121) |
| POST | `/admin/org/<id>/change-plan` | Switch internal/free/basic/pro (admin/routes.py:136) |

### Billing

| Method | Path | Purpose |
|--------|------|---------|
| POST | `/billing/webhook` | Stripe webhook (no CSRF; signature verified) (main.py:5032) |

---

## Data Model

### Tenant schema (per organisation, e.g. `tenant_acme`)

**addons_threads** — Addons thread metadata + enriched zen/support fields (`is_support_expired`, `support_expiration_date`, `zen_nb_days_left`, `customer_language`, `customer_hash`, `transferred_customer_hash`, `qualification`, `nb_messages`, `zen_purchase_date`, `id_product`, `product_quantity`, `product_quantity_refunded`, `has_support`, `support_type`). (models.py:33-73)

**addons_messages** — Per-message rows with `direction` (inbound/outbound), `body_text`, `body_raw`, `hash_dedup` (SHA-256). (models.py:76-104)

**addons_attachments** — `provider_url`, `cached_path` for local copy. (models.py:107-123)

**tickets** — Internal wrapper. `status` (open/in_progress/waiting_customer/answered/closed), `priority` (p1–p4), `is_read`, `is_starred`, `first_response_at`, `reopened_at`, `draft_html` (saved Quill draft). (models.py:126-159)

**tags** + **ticket_tags** — M2M tags. (models.py:161-180)

**ai_suggestions** — Per-message AI output: `summary`, `classification_json`, `draft_reply`, `model_used`. (models.py:183-199)

**ai_suggestion_feedback** — `feedback_type` (thumbs_up/thumbs_down/used_as_is/edited), `edited_text`. (models.py:450-462)

**audit_log** — Tenant audit. Captures actions: `ai_suggestion_generated`, `reply_sent`, `update_status`, `update_priority`, `update_tags`, `marked_as_read`, `marked_as_unread`, `answered_outside`, `dismiss_review_request`, **`credential_reveal`**, **`credential_ftp_test`**. (models.py:202-217)

**addons_products** — Module catalog with `provider_product_id`, `current_version`, `avg_rating`, `total_ratings`, `compatibility_min/max`, `is_online`, `last_update_at`, `base_product_id` (BC→module link), `module_slug`, `billing_active`. (models.py:221-257)

**product_releases** — Uploaded module ZIPs: `version`, `original_filename`, `stored_filename`, `file_size`, `is_latest`. (models.py:260-272)

**predefined_message_categories** — Quick-reply grouping. (models.py:275-288)

**predefined_messages** — `source` (local/addons/ai_generated), `title`, `body`, `iso_lang`, `is_global`, `category_id`, **`attach_latest_module`** (auto-attach ZIP flag). (models.py:289-318)

**product_predefined_messages** — M2M products ↔ messages. (models.py:320-327)

**addons_orders** — API + CSV-import unified table. Includes `csv_imported`, `csv_row_hash` (SHA-256 dedup), `module_version`, `margin_rate`, `net_credit`, `net_refund`. (models.py:330-363)

**addons_customers** — Aggregated profile: `total_orders`, `total_products_purchased`, `total_revenue`, `total_threads`, `country_code`, `language`, `website`. (models.py:365-384)

**app_settings** — Key/value config: `theme_preference`, `auto_close_enabled`, `auto_close_days`, `translation_known_languages`, `translation_preferred_language`, `reply_signature`, `last_csv_import`. (models.py:387-391)

**sync_history** — Sync job state: `sync_type` (recent/historical), `start_page`, `end_page`, `status` (running/paused/failed/completed). Stale `running` rows auto-set to `failed` on startup (main.py:344-355). (models.py:393-410)

**thread_embeddings** — `embedding_blob` (LargeBinary, float32 packed), `dimensions=512`, `text_hash` (SHA-256), `embedding_quality` (no_reply/weak/strong), `summary_text` ("Problem: X. Solution: Y."). (models.py:412-430)

**product_knowledge** — Per-product AI FAQ injected into draft prompt context. (models.py:433-447)

**generated_guides** — Docusaurus MDX articles: `title`, `slug` (unique), `content_md`, links to `ticket_id` and/or `suggestion_id`. (models.py:465-480)

**doc_sources** — External docs to scrape: `url`, `doc_system` (html/docusaurus/markdown/blog), `last_scraped_at`, `page_count`. (models.py:483-497)

**doc_pages** — Scraped pages with embeddings: `content_text` (capped 8,000 chars), `content_hash`, `embedding_blob`. (models.py:500-515)

**message_translations** — Cached translations keyed by (message_id, target_language). (models.py:518-529)

**message_topic_segments** — Per-thread topic boundaries: `segment_index`, `start_message_id`, `end_message_id`, `label`, `summary_text`, `embedding_blob`. (models.py:532-563)

**segmentation_state** — Hash-based cache to skip re-segmentation. (models.py:566-576)

**website_checks** — Per-URL: `is_online`, `module_found`, `modules_found_slugs`, `http_status`, `response_time_ms`, `next_check_after`. (models.py:578-595)

**url_safety_checks** — VirusTotal cache: `url_hash`, `detections`, `total_scanners`, `status` (safe/warning/dangerous). (models.py:597-608)

**customer_sites** — Vault container per customer: `label`, `url`, `notes`. (models.py:610-620)

**site_credentials** — 🔒 **Encrypted**. Per-site credential: `type` (backoffice/ftp/htaccess/cpanel/ssh/other), `username`, **`password_encrypted`** (Fernet text), `extra_json` (FTP mode/host/port, BO URL). (models.py:629-644)

**ai_usage_log** — Per-call AI usage: `call_type` (classify/draft/embed/translate/improve_draft/proofread/shorten/enrich_draft/check_reply), `tokens_used`, `used_own_key`. (models.py:647-666)

### Public schema (shared)

**organizations** — Tenant org: `slug`, `schema_name`, `plan` (internal/free/basic/pro), `stripe_customer_id`, `stripe_subscription_id`, `ai_credits_used`, `ai_credits_reset_at`, 🔒 **`openai_api_key_encrypted`**, 🔒 **`addons_api_key_encrypted`**, `email_verified`, `onboarding_complete`, `is_suspended`, `suspended_reason`. (public_models.py:34-79)

**users** — `email` (unique), bcrypt **`password_hash`**, `role` (platform_admin/admin/agent), `email_verify_token`, `invited_by_id`, `last_login_at`. (public_models.py:81-123)

**plans** — Seeded plan rows: `price_per_module_cents`, `ai_credits_per_month`, `module_limit`, `template_limit`, `features_json`. (public_models.py:126-142)

**public_audit_log** — Platform-level events (suspend/plan changes). (public_models.py:145+)

---

## Services Layer

### `ai_service.py` (~1,100 lines)
Two-call architecture: **classify** (gpt-4o-mini, fast/cheap) then **draft** (gpt-4o, full quality). Methods: `classify_only`, `generate_draft_for_suggestion`, `generate_suggestion`, `improve_draft`, `proofread`, `shorten`, `enrich_draft`, `check_reply`, `translate_text`, `detect_language`, `generate_guide`, `generate_guide_from_summary`. Tracks usage in `AIUsageLog` and increments `public.organizations.ai_credits_used` via cross-schema SQL. Supports **BYOK** OpenAI keys (decrypted at call time, Pro plan).

### `embedding_service.py` (~1,100 lines)
- `EmbeddingIndex` class: in-memory numpy `(N, 512)` float32 array, thread-safe. Loaded from PostgreSQL LargeBinary blobs at startup. Brute-force cosine via `normed @ query_norm` (<5 ms typical).
- Re-ranking: same-product +10% boost, feedback-weighted (+0.15 for thumbs_up / used_as_is, −0.50 for thumbs_down) (embedding_service.py:212-222).
- `search()` (RAG, threshold 0.70), `search_for_user()` (search page, threshold 0.40), `search_docs()` (doc sub-index, threshold 0.30).
- `EmbeddingService`: `embed_thread`, `check_and_embed_missing`, `check_and_upgrade_weak_embeddings`, `get_similar_threads`, `get_guide_candidates` (greedy clustering at 0.82), `count_guide_candidates_batch` (one-pass per-product count with dedup against existing guides ≥0.82 and DocPages ≥0.80, min cluster size 2).

### `sync_service.py` (~1,700 lines)
Message-based incremental sync. **Phase 1**: fetch 200 recent messages → active thread IDs. **Phase 2**: per thread, fetch full detail + messages + attachments. Bleach-sanitised, URL-linkified. Also: `sync_products`, `sync_predefined_messages`, `run_historical_sync` (paginated with pause/resume), `auto_close_old_tickets` (configurable inactivity days). Auto-adds `review-request` tag when a closed ticket receives a "thanks" message.

### `order_sync_service.py` (~900 lines)
`OrderSyncService`: API sync for periods up to 120 months; CSV import via daemon thread with progress callback; SHA-256 row hash dedup; BC → base-module inference; customer-profile rebuilds.

### `billing_service.py`
Stripe customer/subscription lifecycle. Per-module recurring price model (BASIC, PRO). Subscription quantity = count of `billing_active=true` AddonsProducts. Webhook handler resets AI credits on `invoice.paid`. Degrades gracefully if `STRIPE_SECRET_KEY` is unset.

### `credential_service.py`
Lazy-init Fernet singleton from `CREDENTIAL_ENCRYPTION_KEY`. `encrypt_password` / `decrypt_password`. CRUD: `get_customer_sites`, `add_site`, `update_site`, `delete_site`, `add_credential`, `update_credential`, `delete_credential`, `reveal_password` (audit-logged at the route layer).

### `doc_scraper_service.py`
Sitemap-XML driven (`MAX_SITEMAP_PAGES=200`), `MAX_CONTENT_CHARS=8,000`. crawl4ai (`AsyncWebCrawler`, headless Chromium) primary; httpx + bleach text fallback. Each page embedded post-scrape and stored to `doc_pages.embedding_blob`. **Known issue**: in current VPS environment, the bundled Chromium fails with a `libz.so.1 ZLIB_1.2.9` mismatch from `/opt/IDriveForLinux/...`, so scraping currently falls back to httpx for every URL (verified during this session).

### `website_check_service.py`
Parallel httpx HEAD/GET checks (4 workers). Detects module presence via `/modules/{slug}/logo.png`. `run_continuous_checks` is a background loop for periodic re-checks.

### `ftp_test_service.py`
10-second hard timeout. PASV mode. Steps: connect → login → `pwd` → `MLSD` (fallback `LIST`). SFTP via paramiko (auth_password → SFTPClient.listdir). Returns structured `{ok, reason, detail, home}`. Reasons: ok / timeout / auth / network / tls / no_access / no_password / no_host / unsupported / error.

### `url_safety_service.py`
VirusTotal v3 (`/urls/<base64-id>`). 7-day cache. Classification: ≤1 → safe, 2–5 → warning, >5 → dangerous.

### `segmentation_service.py`
TextTiling on message embeddings + time-gap heuristics → topic boundaries. gpt-4o-mini labels each segment. Hash-based cache (`SegmentationState`) skips unchanged threads. Cost estimate ~$0.0004/thread.

### `dashboard_stats.py`
`get_revenue_stats`, `get_customer_geography`, `get_product_performance`, `get_orders_over_time`, `get_customer_value_segments`, `get_period_ticket_stats`, `get_avg_response_hours_3mo`.

### `tenant_service.py`
`provision_tenant`: validates schema name with `^tenant_[a-z0-9_]+$` regex, runs Alembic migrations against the schema, seeds default `AppSettings`.

### `plan_service.py`
Plans:
- **internal**: unlimited everything.
- **free**: 2 modules, 20 AI credits/month, 5 templates.
- **basic**: unlimited modules, 200 AI credits/month, per-module pricing.
- **pro**: unlimited, 1,000 AI credits/month OR BYOK.

Feature flags per plan: order_sync, analytics, vip_badges, tl_dr, ai_badge, topic_segmentation, website_indicator, url_safety, next_ticket, wysiwyg, signature, attachments, product_link, ai_classify, ai_draft, ai_summarize, ai_translate, rag, generated_guides, doc_scraping, semantic_search, website_checks, credentials.

### `auth_service.py`
bcrypt verify + user/org dict load.

---

## AI / RAG / Embeddings

### Models
| Call | Model | Where |
|------|-------|-------|
| Classification | `gpt-4o-mini` | `AI_MODEL_CLASSIFY` (config.py) |
| Full draft / Improve / Enrich | `gpt-4o` | `AI_MODEL_DRAFT` |
| Summarisation / Translation fallback / Proofread / Shorten / Check / Segment labeling / Language detect / Guide-from-summary | `gpt-4o-mini` | ai_service.py |
| Embeddings | `text-embedding-3-small` @ 512 dims | `EMBEDDING_MODEL`, `EMBEDDING_DIMENSIONS` |
| Translation primary | Google Translate `gtx` (free, unofficial) | ai_service.py:641-658 |

### In-memory vector index
- Single global `embedding_index = EmbeddingIndex()` (embedding_service.py module-level singleton).
- Numpy `(N, 512)` float32. Loaded at startup from `thread_embeddings.embedding_blob` (LargeBinary).
- Brute-force cosine. Sub-5 ms search on tested corpora. No external vector DB.
- Doc sub-index: separate `_doc_vectors` array loaded from `doc_pages`.

### Thresholds
| Use | Threshold |
|-----|-----------|
| RAG context (similar tickets injected into draft prompt) | 0.70 (`RAG_SIMILARITY_THRESHOLD`) |
| Search page (user-facing) | 0.40 (`SEARCH_SIMILARITY_THRESHOLD`) |
| Doc sub-index search | 0.30 (embedding_service.py:358) |
| Guide candidate intra-product clustering | 0.78 / 0.82 |
| Cluster ↔ existing-guide dedup | 0.82 |
| Cluster ↔ DocPage dedup | 0.80 |

### Quality labels
`no_reply` (no outbound yet), `weak` (inbound text < 80 chars), `strong` (resolved + sufficient content). Only `strong` embeddings are guide candidates. ~97% of threads are `strong` on the reference tenant.

### Reply generation flow
1. Classify on gpt-4o-mini → `{ summary, priority, qualification, status, could_be_guide, guide_title, language }`.
2. Embed the latest inbound message.
3. RAG search: top 5 similar threads (≥ 0.70), with same-product boost + feedback re-ranking.
4. Doc sub-index search: top 2 (≥ 0.30).
5. Inject style guide + ProductKnowledge + similar threads + doc snippets into gpt-4o draft prompt.
6. Generate draft, store `AISuggestion`, auto-save HTML to ticket `draft_html`.
7. Audit log + AIUsageLog row + org credit increment.

### Guide generation
- **From AI suggestion** (`could_be_guide=true`): full conversation context → gpt-4o.
- **From thread summary**: 1–2 sentence "Problem: X. Solution: Y." → gpt-4o-mini, Docusaurus MDX with frontmatter.
- **Candidate detection** (`count_guide_candidates_batch`): clusters strong threads per product (≥0.78), drops singletons (`min_cluster_size=2`), excludes clusters whose centroid is close to a thread that already produced a guide (≥0.82) **or** a scraped DocPage (≥0.80). Badge on `/products` shows the per-module count.

### BYOK
Pro plan supports own OpenAI API key. Stored encrypted (`openai_api_key_encrypted`) on `organizations`. Decrypted via Fernet at call time, never persisted in plaintext. `used_own_key` flag on each `AIUsageLog` row.

---

## Integrations

### PrestaShop Addons Marketplace
- Provider `ProviderAddons` (custom httpx client) → `https://api-addons.prestashop.com`.
- Synced data: threads, messages, attachments, products, predefined messages, orders.
- Reply send via `provider.send_reply()` with optional binary attachment payload.
- Threads enriched with `zen` (support expiration) and BC (`support`) fields.

### Email
Flask-Mail SMTP. Used for registration verification + team invites. No IMAP/Gmail ingest — all ticket data comes from the Addons API.

### Stripe
- Stripe Python SDK ≥7.0.0. Per-module recurring pricing. Subscription `quantity` mirrors `billing_active` module count.
- Webhook: `/billing/webhook` with signature verification (CSRF-exempt). Handles `invoice.paid` → reset AI credits.
- Optional: degrades gracefully without `STRIPE_SECRET_KEY`.

### paramiko
SFTP testing only. No SSH/SFTP file transfer features.

### crawl4ai + Playwright
- crawl4ai for doc scraping (headless Chromium).
- Playwright also used directly in `main.py` to fetch attachments behind Cloudflare (`AutomationControlled` disabled, custom UA, handles both page-response and download streams). (main.py:1548-1610)

### VirusTotal
URL safety checks. 7-day cache in `url_safety_checks`. Used to add caution badges on customer-supplied URLs across the UI.

### Google Translate (unofficial)
Free `gtx` endpoint, no API key. Falls back to gpt-4o-mini if it fails.

---

## Background Jobs / Scheduling

### In-process daemon threads (active path)
| Job | Trigger | Frequency | Source |
|-----|---------|-----------|--------|
| Auto-sync (threads + products + predefined msgs) | `@before_request` one-shot start | every 30 min | main.py:4977-5003 |
| Website check loop | `@before_request` one-shot | continuous w/ per-URL `next_check_after` | main.py:5009-5023 |
| Daily embedding catch-up + weak upgrade | `@before_request` w/ 24h TTL | daily | main.py:4943-4971 |
| Post-reply re-embed | per reply send | once | main.py:1391-1398 |
| Historical sync | manual `/start-historical-sync` | one-shot | main.py:3047 |
| Doc scrape (per-source or all) | manual | one-shot | main.py:3349 |
| CSV import + customer rebuild | manual upload | one-shot, with progress poll | main.py:4234 |

### Celery (prepared multi-tenant path; not the active runtime in dev)
| Task | Retries | Source |
|------|---------|--------|
| `sync_threads` | 3 | tasks/sync_tasks.py:20 |
| `run_historical_sync` | 2 | tasks/sync_tasks.py:64 |
| `generate_suggestion` | 2 | tasks/ai_tasks.py:17 |
| `generate_embeddings` | 2 | tasks/ai_tasks.py:53 |
| `reset_free_tier_credits` | — | tasks/billing_tasks.py:14 |

### Celery Beat
`reset-free-tier-credits-monthly`: `crontab(minute=5, hour=0, day_of_month=1)` — zeroes `ai_credits_used` for all free-plan orgs. (celery_app.py:51-56)

---

## Admin / Platform Console

Blueprint `/admin/*`, gated by role `platform_admin`. Org listing with user counts, plan, suspension state. Per-org drill-in shows users, recent audit log, AI usage from the tenant schema. Actions: suspend/unsuspend with reason, change plan among internal/free/basic/pro. Each admin action writes to `public.public_audit_log`.

---

## Security & Auth

| Layer | Implementation |
|-------|---------------|
| Authentication | Flask-Login, bcrypt password hash. 30-day permanent session cookie `supporthub_session` (main.py:297-300). |
| CSRF | Flask-WTF `CSRFProtect(app)`. `WTF_CSRF_TIME_LIMIT=None` (validity tied to session secret). Single exemption: `/billing/webhook`. |
| Roles | `platform_admin`, `admin`, `agent`. `@require_role('admin', 'platform_admin')` decorator. |
| Tenant isolation | PostgreSQL schema-per-tenant. `session_scope()` issues `SET search_path = <schema>` (main.py:413-417). |
| Schema-name validation | `re.compile(r'^tenant_[a-z0-9_]+$')` before any `CREATE SCHEMA`. (tenant_service.py:20) |
| Audit logs | Tenant `audit_log` for: AI suggestion, reply send, status/priority/tag changes, read/unread flips, credential reveals, **credential FTP tests**. Public `public_audit_log` for org-level admin actions. |
| Encryption at rest | Fernet (symmetric) via `cryptography`. Key from `CREDENTIAL_ENCRYPTION_KEY` env. Protects: `site_credentials.password_encrypted`, `organizations.openai_api_key_encrypted`, `organizations.addons_api_key_encrypted`. |
| HTML sanitisation | bleach on every user-supplied reply and template body. Allowed: `p, br, strong, b, em, i, u, ul, ol, li, a`. (main.py:1242) |
| Rate limiting | Not currently implemented; no `flask-limiter`. |
| Stripe webhook | Signature-verified, CSRF-exempt. |

---

## Ticketing Workflow

### Statuses & priorities
Statuses: **open → in_progress → waiting_customer / answered → closed**. Priorities: p1 (Critical), p2 (High), p3 (Normal, default), p4 (Low). AI assigns priority on classify call.

### Smart priority scoring (main.py:163–253)
Integer score combining:
- Unread + no first response: +500
- Unread + had response: +250
- Read + no response: +150
- Priority weight: p1=100, p2=75, p3=50, p4=25
- Pre-sale (no prior order): +100
- "Thanks" pattern in last inbound: +60
- Wait days × 2
- Support expiring < 30 days: +25; already expired: −15; refunded order: +20
- Qualification: complaint +30, bug +25, question +10, feature_request +5
- Closed ticket: −1000 (floor)

### Reply pipeline
1. Quill WYSIWYG → plain-text-with-paragraphs conversion compatible with Addons. (main.py:1246-1269)
2. `provider.send_reply()` to Addons API, optional attachment.
3. `_classify_reply_status()` regex auto-classifies sent reply → `waiting_customer` / `answered` / `dialog_waiting` (`?` end) / `dialog_answered` (ambiguous; UI shows confirm popup). Patterns include credential-request keywords for "waiting" and "let me know if this helps" / "problem is fixed" for "answered".
4. First-response timestamp set, draft cleared, review-request tag removed, post-reply re-embed kicked off.

### Attachments
- Per-file cap: 10 MB. (main.py:1278)
- Multi-file: auto-zipped server-side as `attachments.zip`. Total cap 50 MB. (main.py:1282-1296)
- Local cache directory `instance/attachment_cache/`. Playwright bypass for Cloudflare on Addons URLs.

### AI draft persistence
`tickets.draft_html` (Text). GET/POST `/ticket/<id>/draft` for autosave/restore. Cleared on send.

### Predefined messages
- Source: local / addons (synced) / ai_generated (saved from suggestion).
- Global or product-bound (M2M).
- `attach_latest_module=True` auto-attaches latest uploaded `product_releases` ZIP.
- `[Review]`-prefixed messages auto-selected by `/api/review-request-template` matched on the customer's language.

### Auto-close
Configurable inactivity days (`auto_close_days`). Closure executed at end of each sync run (sync_service.py:1282). Closed tickets receiving a "thanks" auto-add `review-request` tag.

---

## Customer / In-app Features

### Search
- `/api/search` combines semantic (cosine ≥ 0.40) with SQL `ILIKE` keyword in one result set.
- Filters: product_id, status. `top_k=25`.
- `extended=1` falls back to keyword-only when embeddings are unavailable.

### Quick reply categories
Grouped browser in ticket view, organised by `predefined_message_categories.sort_order`.

### AI Composer toolbar
Inside the ticket Quill editor: Write, Improve, Proofread, Shorten, Enrich, Check Reply. All AJAX. Suggestion + draft saved to `AISuggestion`. Thumbs up/down/used_as_is/edited feedback shapes future RAG re-ranking.

### Topic segmentation
TextTiling + time-gap heuristics on the message stream. Labelled segments shown as sticky headers in the thread view and as a sidebar mini-nav. Hash-cached so unchanged threads skip cost.

### TL;DR banner
For threads with ≥5 messages, surface `ThreadEmbeddings.summary_text` at the top. (main.py:1003-1010)

### VIP badge
Customer revenue above threshold → VIP badge in inbox + ticket sidebar.

### Review-request flow
Closed + unread + `review-request` tag bubbles up via dedicated sidebar badge. One-click templated reply via `/api/review-request-template`.

### Next ticket suggestion
Post-reply popup recommends the next ticket by mode: `quick` (shortest inbound first) or `priority` (oldest highest-priority).

### Language detection + translation
gpt-4o-mini zero-shot detection (ISO 639-1). Inline per-message translate button (cached). Uncached draft translation for outbound. Preferred language stored in AppSettings.

### Site health & module-presence indicators
Customer URLs auto-checked. Visible in inbox, customer list, order list, customer detail, ticket sidebar. Indicator colours map to online/module-installed/module-missing states.

---

## Credential Vault

A per-customer encrypted password manager designed for the day-to-day reality of giving support on shops where you frequently need BO / FTP / cPanel / SSH access:

- Sites are grouped per customer (`customer_sites`); each site holds N credentials (`site_credentials`).
- Credential types: `backoffice` / `ftp` / `htaccess` / `cpanel` / `ssh` / `other`.
- Passwords encrypted with **Fernet** (key in env). Reveal endpoint logs to `audit_log`.
- BO credentials store the back-office URL in `extra_json.bo_url`; FTP credentials store `mode` (ftp/ftps/sftp), `host`, `port`.
- Add/edit form: Type → URL → User → Password → optional Label (revealed via "+ Add a label" toggle to reduce visual noise).
- **Live FTP/FTPS/SFTP test**: connect + login + list the home directory. Per credential. 10-second timeout. PASV. Auto-runs after save. Returns one of: ok / timeout / auth / network / tls / no_access / no_password / no_host / unsupported / error. On success the home folder path is displayed. Audit-logged.
- Copy-Connection one-click → `sftp user@host:port` / `ftp ...` to clipboard.
- Quick Login button on BO credentials opens the back-office URL and copies the password.

---

## Documentation Scraping & Guide Generation

- Add a doc source URL on Settings page. The scraper auto-discovers `sitemap.xml` at site root or accepts a direct sitemap URL.
- Up to **200 pages per source** indexed. Each page capped at 8,000 chars for embedding.
- crawl4ai (Chromium) primary; httpx + bleach fallback. **Note**: on environments where Chromium can't launch (e.g. VPS ZLIB conflict), the scraper falls back to httpx for everything, which works for static / SSR sites but yields thin content for JS-only Docusaurus category pages.
- Pages embedded into a separate `_doc_vectors` numpy sub-index, used for both RAG injection during draft and `count_guide_candidates_batch` dedup.
- Guides created via "✨ Generate Guide" on the products page are Docusaurus MDX with frontmatter (`id`, `title`, `sidebar_label`). They live in the `generated_guides` table and are listed at `/guides`.
- Per-product "+ N possible" amber badge on `/products` indicates clustered candidate topics that are neither already a generated guide nor already covered by a scraped DocPage.

---

## Saved Replies / Predefined Messages

- Three sources: local (created in app), addons (synced from Marketplace), ai_generated (saved from AI suggestion).
- Categories with sort order, plus an Uncategorised bucket.
- Global or product-bound (M2M).
- ISO language tag for language-aware auto-selection.
- `[Review]`-prefixed special handling for review-request templates.
- Optional auto-attach of latest module release ZIP on send.

---

## Audit Log

Per-tenant `audit_log` rows are created at every business-meaningful event. Actions recorded (non-exhaustive):

- `ai_suggestion_generated` — AI classify/draft cycle.
- `reply_sent` — payload includes target status, char count, attachment count.
- `update_status`, `update_priority`, `update_tags` — manual changes.
- `marked_as_read`, `marked_as_unread`.
- `answered_outside`, `dismiss_review_request`.
- `credential_reveal` — payload `{cred_id}`.
- `credential_ftp_test` — payload `{cred_id, host, mode}`.

Public-schema `public_audit_log` captures org suspend/unsuspend/plan changes by `platform_admin` users.

---

## Multi-Tenancy

- **One PostgreSQL database, one schema per tenant**: `tenant_<slug>`.
- The `public` schema holds `organizations`, `users`, `plans`, `public_audit_log` (cross-tenant data).
- `session_scope()` issues `SET search_path = tenant_<slug>, public` before any query in the request, then resets to `public` on teardown so connection-pool reuse doesn't bleed schemas. (db.py:48-68)
- Schema names validated via `^tenant_[a-z0-9_]+$` before any DDL.
- `tenant_service.provision_tenant()`: `CREATE SCHEMA IF NOT EXISTS`, runs Alembic upgrade against the new schema, seeds default `AppSettings`.
- Bootstrap scripts exist under `scripts/` (see `memory/multitenant-postgres.md` for usage notes).

---

## Deployment & Ops

- **Production**: Phusion Passenger on cPanel/CloudLinux. Entry `passenger_wsgi.py`. CI/CD via GitHub Actions FTP/SFTP deploy.
- **Reverse proxy**: Caddy → `support.claude-baby.dev`, TLS terminated by Caddy. (memory/caddy-setup.md)
- **Local/VPS dev**: systemd-managed Flask service auto-restarts; `--debug` enables stat-based hot reload (CLAUDE.md project rules forbid manual restart unless deps or schema change).
- **Env**: see `config.py:37-116` for the full list; DB connection, AI keys, Stripe keys, mail server, RAG/embedding tuning, signature, module upload path, encryption key, VirusTotal key.
- **Startup hygiene**: stale `sync_history` rows with `status='running'` are flipped to `failed` on app boot. (main.py:344-355)
- **Module uploads**: `MODULE_UPLOADS_PATH` (default `uploads/modules/<product_id>/`); UUID storage filename, original filename preserved in DB.
- **Attachments**: cached in `instance/attachment_cache/`.

---

## Key Differentiators

1. **Built specifically for the PrestaShop Addons Marketplace** — direct ProviderAddons API integration (threads / messages / attachments / products / predefined messages / orders), Cloudflare-bypass attachment fetching with Playwright, support-expiration (`zen`) and Business Care fields surfaced everywhere.
2. **Two-pass AI architecture** — gpt-4o-mini classifies (cheap, fast), gpt-4o drafts (full quality). Five extra one-click operations on any draft: Improve, Proofread, Shorten, Enrich, Check Reply (coverage QA).
3. **In-memory numpy RAG, no external vector DB** — 512-dim text-embedding-3-small vectors brute-force searched in RAM, <5 ms. Same-product +10% boost. Feedback re-ranking (+0.15 / −0.50) shaped by thumbs up/down/used_as_is/edited (`ai_suggestion_feedback`).
4. **Generated documentation guides clustered from real resolved tickets** — `count_guide_candidates_batch` finds clusters of strong-quality threads (≥0.78 similarity, min size 2) that are not yet covered by an existing guide (≥0.82) or a scraped DocPage (≥0.80). Badge on the products page surfaces the count.
5. **Encrypted per-customer credential vault with live FTP test** — Fernet-encrypted passwords, support for backoffice / FTP / FTPS / SFTP / cPanel / SSH / other. Live connect+login+home-folder listing per credential with reason-code error reporting (timeout / auth / TLS / network / no_access). All reveals and tests audit-logged.
6. **Customer site health + module-presence monitoring** — background loop checks every customer URL, detects whether *your* modules are installed via `/modules/{slug}/logo.png` probing, surfaces colored indicators in inbox, customer list, customer detail, orders, and the ticket sidebar.
7. **Smart priority scoring with 10+ signals** — unread/response state, qualification, pre-sale flag, support expiry, refunds, "thanks" detection, wait days. One sort, all signals.
8. **Topic segmentation per thread** — TextTiling + time-gap heuristics + gpt-4o-mini labels, presented as sticky in-thread headers and a sidebar mini-nav. Hash-cached to ~$0.0004/thread.
9. **Schema-per-tenant PostgreSQL** with `search_path` switching at session level, plus public-schema billing / users / plans. No row-level mixing across orgs.
10. **BYOK OpenAI key** for Pro plan — your key, your data residency. Stored Fernet-encrypted, decrypted only at call time. Per-call `used_own_key` flag in `ai_usage_log`.
11. **Cached message translation + free Google Translate primary** — unofficial `gtx` endpoint as primary (no API key), gpt-4o-mini fallback. Per-language cache in `message_translations`.
12. **Module ZIP hosting + auto-attach** — versioned uploads (10 MB each, 50 MB zipped total), `attach_latest_module=true` flag on predefined messages, one-click "Attach Module" in the ticket composer.
13. **CSV import for full order history** — up to 10-year backfill via the Addons CSV export. Background daemon with real-time progress polling. SHA-256 row-hash dedup against API-synced orders.
14. **VirusTotal URL safety badges** — every customer-supplied URL across the UI gets a cached caution badge for `warning` / `dangerous` classifications.
15. **Smart reply-status classification** — sent reply is auto-classified into `waiting_customer` / `answered` via regex on the body, with a UI confirm popup for ambiguous cases.

---

## FAQ

### About AI

**Q. How does the AI reply suggestion work?**
On each inbound message, ModuleDesk runs a two-call pipeline. First, gpt-4o-mini classifies the message (priority, qualification, status, language, guide-worthy flag, summary). Second, gpt-4o writes a draft, primed with: your style guide, AI-generated product knowledge for that module, up to 5 similar resolved tickets retrieved via RAG (cosine ≥ 0.70), and up to 2 doc snippets from your scraped documentation. The draft auto-saves to the ticket. (ai_service.py; main.py:3119)

**Q. What AI models do you use?**
gpt-4o for full drafts and the Improve/Enrich operations; gpt-4o-mini for classification, summarisation, translation fallback, proofread, shorten, language detection, guide generation from summaries, and segment labeling; `text-embedding-3-small` at 512 dimensions for all embeddings. Translation primary is Google's free unofficial `gtx` endpoint. (config.py:80-92)

**Q. Can I use my own OpenAI key?**
Yes, on the Pro plan. Your key is encrypted (Fernet) inside `public.organizations.openai_api_key_encrypted` and decrypted only at call time. Each `ai_usage_log` row records `used_own_key=true` for billing/audit. (ai_service.py:43-64)

**Q. What's the AI Composer toolbar?**
Buttons in the reply editor that act on the current draft: Write (fresh draft), Improve (rewrite), Proofread (fix typos/grammar), Shorten, Enrich (add facts from RAG), Check Reply (verifies your draft answers every question in the inbound message).

**Q. How do guides get generated?**
Two ways. (1) When AI classification flags a Q&A as "guide-worthy," a button on the ticket spawns a Docusaurus MDX article. (2) On the products page, click the lightning bolt to see clusters of similar strong-quality tickets that don't yet have a guide and aren't covered by your scraped docs — pick one and generate. The "+N possible" amber badge on each product row shows the cluster count waiting for you. (main.py:3452, 3672, 3684)

**Q. How does similar-ticket search rank results?**
Cosine similarity (numpy in-memory index) with two boosts: same-product +10%, and a feedback weight shaped by your previous thumbs (+0.15 for thumbs up / used_as_is, −0.50 for thumbs down) on `AISuggestion` rows. (embedding_service.py:212-222)

### About security & data

**Q. How are passwords stored?**
User account passwords are bcrypt-hashed. Stored credentials inside the customer vault (BO / FTP / cPanel / SSH passwords) are encrypted with Fernet (symmetric AES-128-CBC + HMAC), key from the `CREDENTIAL_ENCRYPTION_KEY` env. Same encryption protects org-level `openai_api_key_encrypted` and `addons_api_key_encrypted`. (credential_service.py)

**Q. Who can reveal a stored password?**
Any logged-in user, but every reveal is audit-logged (`credential_reveal`) with user, IP, timestamp, credential id. FTP tests are also audit-logged (`credential_ftp_test`). (main.py:2429-2445; 2447-2475)

**Q. How is tenant data isolated?**
One PostgreSQL database, one schema per tenant (e.g. `tenant_acme`). On every request, the session's `search_path` is set to the active tenant schema. The `public` schema holds only shared platform data (orgs, users, plans, platform audit log). Tenant schema names are validated by regex (`^tenant_[a-z0-9_]+$`) before any DDL. (db.py:48-68; tenant_service.py:20)

**Q. Is there CSRF protection?**
Yes, Flask-WTF `CSRFProtect` is enabled globally. The only exemption is `/billing/webhook`, which is authenticated by Stripe's signature scheme instead. (main.py)

**Q. Is there rate limiting?**
Not currently. There's no `flask-limiter` in the dependency stack.

### About integrations

**Q. Does it work with PrestaShop Addons Marketplace?**
Yes — it's built for it. The product fetches threads, messages, attachments, products, predefined messages and orders directly from `api-addons.prestashop.com`, including expiration / Business Care fields. Replies are sent back through the same API.

**Q. Does it work with Shopify / WooCommerce / Stripe Atlas?**
No. Single-purpose for PrestaShop Addons sellers.

**Q. What about email (IMAP/Gmail)?**
ModuleDesk does **not** ingest email. Every message arrives via the Addons API. Outbound mail is used only for registration verification and team invites (Flask-Mail SMTP).

**Q. How does billing work?**
Stripe per-module subscription. Plans: internal (free, unlimited for you), free (2 modules, 20 AI credits), basic (unlimited modules, 200 credits, paid per module), pro (unlimited, 1,000 credits or BYOK). Subscription `quantity` mirrors the count of modules with `billing_active=true`. Webhooks reset AI credits on `invoice.paid`. (plan_service.py; billing_service.py; main.py:5032)

**Q. Where does FTP testing run from?**
From the ModuleDesk server. That means servers with IP allow-lists, VPN-only access, or internal-network-only FTP won't be testable from here — the test returns `reason: "network"` or `"timeout"` so you know it's unreachable rather than wrong-credentials.

**Q. What does the FTP test actually do?**
Connects (10s timeout), authenticates, calls `pwd`, then lists the home directory with `MLSD` (falling back to `LIST`). For SFTP it uses paramiko (password auth only; no key support yet — a missing password on SFTP returns `reason: "no_password"` with a hint about key-based auth). Results: ok / timeout / auth / network / tls / no_access / no_password / no_host / unsupported / error. (ftp_test_service.py)

### About data & sync

**Q. Can I import my historical orders?**
Yes. Either via API up to 120 months back, or by uploading the Addons orders CSV. CSV import dedups against API-synced orders using SHA-256 row hashes. Customer profiles (revenue, threads, orders) auto-rebuild after import. (main.py:3932, 4129)

**Q. How often does it sync?**
Every 30 minutes automatically (in-process daemon). Manual sync from the inbox is available any time. Full historical backfill is a pausable/resumable batch job triggered by an admin. (main.py:4977-5003)

**Q. Are background jobs ready for multi-tenant scale?**
Celery tasks and a Beat schedule are defined (sync_threads, run_historical_sync, generate_suggestion, generate_embeddings, reset_free_tier_credits) for the multi-tenant runtime; the dev/single-tenant path uses in-process daemon threads. (tasks/*.py; celery_app.py)

### About admin & teams

**Q. Is it multi-user?**
Yes. Each org can have multiple users with three roles: `agent` (default), `admin` (manage settings, templates, doc sources, products, sync), `platform_admin` (cross-org platform console). Invites are token-link emails. (main.py:2848; onboarding/routes.py:449)

**Q. What's audited?**
At the tenant level: AI suggestions, replies, status/priority/tag changes, read/unread flips, credential reveals, credential FTP tests, "answered outside the app" markings, review-request dismissals. At the platform level: org suspensions and plan changes.

**Q. Can I add my own tenants?**
Yes — registration creates a new organisation, provisions a new `tenant_<slug>` schema, runs the migrations, seeds defaults, and walks the user through API-key + plan + AI onboarding.

**Q. What if Stripe isn't configured?**
The app degrades gracefully: billing endpoints become no-ops and the free/internal plans continue to work. Setting `STRIPE_SECRET_KEY` later activates paid plans. (billing_service.py)

### About the doc base

**Q. How do I scrape my docs?**
Settings → add a doc source URL. We auto-discover `sitemap.xml` at site root and index up to 200 pages. Each page is embedded so it becomes part of the AI's context whenever you draft a reply for a similar topic. (doc_scraper_service.py; main.py:3277)

**Q. Why am I only seeing 44 of my 65 doc pages?**
A known environmental issue: when the bundled headless Chromium can't launch (e.g. a ZLIB conflict from a co-located IDrive install), the scraper falls back to httpx and bleach. That works fine for static HTML and SSR Docusaurus pages, but Docusaurus category index pages render their content client-side via JS, so they come out short or empty. Static module pages scrape fine. (Verified during this session.)

### About translation

**Q. Can it translate customer messages?**
Yes. Each message has a per-language translate button; results are cached in `message_translations`. Drafts can be translated outbound (uncached) for sending in the customer's language. Primary translator is Google's free unofficial `gtx` endpoint; gpt-4o-mini is the fallback. (ai_service.py:641-658; main.py:2888, 2954)

### About the search

**Q. Is the search semantic or keyword?**
Both. `/api/search` returns a merged list of semantic results (cosine ≥ 0.40 across all your threads, top 25) and SQL `ILIKE` keyword matches. When the embedding service is unavailable, `extended=1` falls back to keyword-only. (main.py:895)

### About attachments

**Q. What's the attachment limit?**
10 MB per file, 50 MB total. If you select multiple files, they're auto-zipped server-side as `attachments.zip` before being sent to the Addons API. (main.py:1278-1296)

**Q. Why do attachments sometimes go through a different fetcher?**
The Addons attachment URLs are sometimes behind Cloudflare. When the direct HTTP request fails, we transparently fall through to Playwright (headless Chromium with `AutomationControlled` disabled) which receives the file either as a page response or a triggered download. Result is cached locally. (main.py:1548-1610)

---

## Version / Migration History (latest 10)

| Revision | Purpose |
|----------|---------|
| `z7a8b9c0d1e2` | Add url_safety_checks + credential vault tables (`customer_sites`, `site_credentials`) |
| `y6z7a8b9c0d1` | Add `cached_path` to attachments (local cache support) |
| `x5y6z7a8b9c0` | Add `csv_row_hash` for CSV import dedup |
| `w4x5y6z7a8b9` | Drop legacy `customer_id` uniqueness |
| `v3w4x5y6z7a8` | Add `modules_found_slugs` to website_checks |
| `u2v3w4x5y6z7` | Add website_checks table + module_slug on products |
| `t1u2v3w4x5y6` | Add `bc_base_product_id` (Business Care → module link) |
| `s0t1u2v3w4x5` | Add CSV order fields (margin_rate, net_credit, net_refund, etc.) |
| `r9s0t1u2v3w4` | Add `summary_text` to message_topic_segments |
| `q8r9s0t1u2v3` | Add segmentation tables (`message_topic_segments`, `segmentation_state`) |

(33 migrations total under `supporthub/migrations/versions/`.)

---

*Generated by `/app-facts`. Re-run after major feature additions to keep this current.*
