# Pre-Sale Feature Review — June 2026

Full-product review of every feature area (code + screenshots), done 2026-06-13.
Quick wins were implemented and committed (945b054, d75a145, 320fc69). This file
is the remaining backlog, ordered by priority.

## Per-feature verdicts

| Feature | Verdict |
|---|---|
| Inbox, Ticket view, AI composer, Sync | Essential — core loop is solid |
| Dashboard, Customers, Orders, Credential vault, Website checks | Essential |
| Predefined messages (Quick Replies), Search | Essential |
| Products (releases/ZIP hosting) | Essential — ZIP hosting is a differentiator |
| Ratings | Useful |
| Billing/Onboarding | Essential (Checkout flow bug fixed in d75a145) |
| Guides | **Marginal as-is** — needs the rework below |
| Auth | Adequate but missing critical flows (below) |

## P1 — Should do before selling

1. **Password reset flow** — does not exist. A user with a typo'd password is
   permanently locked out (registration has no confirm-password field either).
   Implement /forgot-password → email token → reset form; add confirm-password
   to registration. Generates support load and churn otherwise.
2. **Resend verification email** — verify_email_sent.html is a dead end; if the
   mail lands in spam the signup is stuck. Add rate-limited POST /resend-verification.
3. **Login brute-force protection** — login form has unlimited attempts (the
   API-key step already has Redis rate limiting; reuse the pattern).
4. **Guide detection rework** (owner already flagged proposals as nonsensical):
   - Root cause: `could_be_guide` is set by the classify call on the *incoming,
     unanswered* message — the AI judges guide-worthiness before any solution exists.
   - Products-page candidate counts (146/172 per product) come from greedy cosine
     clustering of 1–2 sentence summaries at 0.78 threshold — too loose.
   - Rework: drop `could_be_guide` from classify; trigger candidacy post-resolution
     (ticket closed + embedding_quality == 'strong'); remove the
     `if not could_be_guide: 400` gate on manual generation (main.py ~3908);
     raise generate_guide_from_summary max_tokens (800 → 1500, guides truncate).
5. **Bulk inbox actions** — no way to bulk-close/retag 30 identical tickets after
   a release. Checkbox column + bulk-action bar + one POST endpoint.

## P2 — High value, medium effort

- **Onboarding visual split**: register/onboarding use Bootstrap, the app uses
  Tailwind — jarring jump in the conversion funnel. Migrate templates.
- **Landing page**: no screenshots/demo video, no real testimonials, prices
  hardcoded (€19/€49) instead of injected from PLAN_PRICE_CENTS.
- **Reply status classification is English-only** (`_classify_reply_status`
  regexes) — French/Spanish replies always classify as "answered".
- **Auto-sync runs per worker process** — with multi-worker gunicorn, N sync
  loops race. Use a PostgreSQL advisory lock (also applies to in-process progress
  dicts for website checks / CSV import — fine single-process, breaks multi-worker).
- **Incremental sync early-stop**: `consecutive_skips >= 5` can abandon remaining
  active threads; no catch-up after server downtime > 200 messages.
- **Customer detail page lacks credential vault access** — vault only reachable
  from tickets; add section reusing showCredentialModal(customer_hash).
- **rebuild_customer_profiles N+1** revenue query per customer; full rebuild runs
  after every sync — pass only_hashes like the CSV import does.
- **Amount/revenue stored as String(50)** (AddonsOrder.amount,
  AddonsCustomer.total_revenue) — migrate to Numeric(10,2), removes CAST hacks.
- **Plan-flag naming**: orders CSV + dashboard geo/segment charts are gated on
  `ratings_export`; dashboard CSV export needs MAX while its charts are Premium.
  Introduce `analytics_advanced` flag and align.
- **Template placeholders**: predefined messages only substitute {product_name};
  add {customer_name}, {order_id}, {module_version}. Body is bleach-stripped to
  plain text — allow a safe tag whitelist.
- **Doc-source scraping**: no progress feedback, no periodic re-scrape schedule,
  no staleness warning.
- **Credential vault hardening**: missing CREDENTIAL_ENCRYPTION_KEY silently
  stores plaintext (surface an admin banner); parse-paste hardcodes gpt-4o-mini
  (bypass of BYOK); site/cred routes lack explicit ownership checks (currently
  protected only by schema isolation).
- **Draft reply max_tokens=600** truncates long technical replies — raise to ~900.
- **Search quality**: 0.40 similarity threshold too permissive (try 0.55–0.65);
  keyword leg doesn't search message body_text; hardcoded suggestion chips are
  catalog-specific; free-plan degradation to keyword-only is silent.
- **Website checks**: offline sites recheck every batch (add backoff);
  module-found vs request-failed conflated; could detect installed module version
  via config.xml.
- **2FA/TOTP** — increasingly expected for a tool holding customer credentials.

## P3 — Polish / noted

- Search has a deliberate fake 2–4s "thinking" delay on extended search
  (search.html ~256) — results are held back in JS. Review whether intended.
- Inbox row markup duplicated between inbox.html and _inbox_rows.html (partial is
  the *more* complete one — server-rendered rows lack the review-request "Thanks"
  badge). Extract a shared macro.
- Ticket page: auto language detection is a synchronous AI call on first open;
  read tickets don't scroll to last message; attachment send forces full refresh.
- Ratings: tmp/ratings_import_latest.json debug dump on every import (grows
  unbounded); DE/ES/IT new-format locales unmapped; "No stars" warning text
  describes the old workflow; ratings list page has no pagination (43k px tall).
- Settings: signature is plain-text while composer is rich text; language
  datalist only 10 languages.
- Team: no resend-invite; role change has no confirm.
- Billing page: no renewal date shown; no low-credit warning outside the page;
  chart toggles in localStorage are global not per-org; /billing/topup has no
  server-side plan check.
- Dashboard: value segments use lifetime revenue, not the date filter;
  product-performance N+1.
- products.html: guide-candidate clustering runs on every page load (add a 60s
  TTL cache); Module ZIP column hidden below xl breakpoint.
- decrypt_password returning ciphertext on InvalidToken is *deliberate* (legacy
  plaintext support) — but a rotated key silently returns garbage; consider
  distinguishing the cases.
- Multi-tenant deployment risks (currently masked by single-process):
  embedding index singleton is shared across tenants; auto-sync `g.tenant_schema`
  is always empty in the background thread.
