Skip to content

Tags: chattermate/chattermate.chat

Tags

v1.0.15

Toggle v1.0.15's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #211 from chattermate/claude/agent-login-mobile-pw…

…a-ce014a

feat(mobile): installable agent PWA + agents can see the AI queue

v1.0.14

Toggle v1.0.14's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
FAQ Manager & public Help Center (auto-generated from knowledge base) (

…#191)

* feat(help-center): FAQ + help center data layer (models, migration, repos, schemas)

Foundations for the FAQ manager and public help center:
- faqs, help_center_settings, faq_generation_jobs, help_center_queries tables
  (string statuses like knowledge_queue; domain verification state derived
  from per-record booleans; FK indexes for delete paths; partial index for
  the worker poll)
- FAQ_GENERATED / FAQ_GENERATION_FAILED notification types (enum values
  added via ALTER TYPE; downgrade removes referencing rows)
- org-scoped repositories for FAQs, settings, ask-metering and jobs
- pydantic schemas with shared URL normalizer (IDN-safe custom-domain
  validation, https-only import URLs, partial FAQ updates) and a shared
  Pagination model
- jinja2 + dnspython dependencies; help-center config (base domain,
  reserved slugs)

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): FAQ generator agent + shared Groq structured-output helpers

- extract the Groq json-tool structured-output machinery from chat_agent
  into app/agents/structured_output.py (generalized tool builder; chat_agent
  keeps thin aliases so call sites and tests don't churn)
- upgrade lenient_json_load to bracket-aware repair so truncated tool calls
  with nested arrays (FAQ lists) salvage correctly, and complete tool calls
  with trailing wrapper braces parse via raw_decode
- FAQGeneratorAgent: one-shot grounded FAQ extraction from knowledge text
  and verbatim Q&A extraction from external FAQ pages, using the org's
  configured model; Groq uses the shared json-tool path with salvage,
  malformed items are dropped per-entry rather than failing the batch

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): FAQ generation + import pipeline, worker and auto-generation hook

- faq_generation service: reads stored knowledge text per page (natural chunk
  ordering, crawl-order page cap), greedy-packs whole pages into LLM batches,
  dedups against existing questions (normalized), merges categories
  case-insensitively, inserts drafts; per-source read failures skip rather
  than abort; caps are env-configurable (FAQ_* settings)
- faq_import service: SSRF-guarded static fetch (browser fallback only on the
  hop-validated final URL, run off the event loop), block-aware text
  extraction so Q&A pairs never split across batches; shares the drafting
  loop, dedup and batching with generation
- faq_processor worker: independent loop beside the knowledge loop (a long
  FAQ job never delays ingestion; FAQ-stack import failure only disables the
  FAQ half); session rollback before mark_failed so failed jobs can't be
  stuck in processing
- auto-generation hook on knowledge completion: adopted orgs only, honors
  auto_generate + plan gate, dedups active jobs, deterministic source lookup,
  never raises and never poisons the caller's session
- generic feature_gate service (help_center gate is a thin wrapper; existing
  per-feature gate copies can migrate later); shared notify_user helper
- fix pre-existing bug: knowledge notifications passed metadata= (shadowed
  Base.metadata, silently unpersisted) instead of notification_metadata=

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): admin API (settings/branding, FAQ CRUD, generation jobs) with Pro gating

- /api/v1/help-center router package: settings get-or-create (slug from org
  name with reserved-label + collision handling, agent auto-default),
  branding updates with validation, logo upload (content-type-derived
  extensions only, SVG active-content screen, shared store_upload backend +
  signed S3 URLs), FAQ CRUD with org-scoped 404s and partial updates,
  bulk publish/unpublish, generate/import enqueue with 409 duplicate guard
  (DB-enforced via partial unique index — check-then-insert races safely),
  job polling endpoints
- shared file_storage service (S3-or-local + public URL resolution) fixing
  the local-mode URL to match the /api/v1/uploads static mount
- all mutations gated by the help_center Pro flag (OSS unrestricted);
  settings GET stays ungated and reports plan_allowed for the lock UI
- API tests incl. simulated-cloud gating (403 on Free, allowed on Pro)

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): public SSR help center with host dispatch, Ask AI and widget embed

- host-dispatch ASGI layer: {slug}.<base domain> and DB-verified custom
  domains route to a standalone public app; verified-domain cache refreshes
  in the background (never blocks the request path); host parsing shared
  between dispatcher and resolver
- SEO-first Jinja2 page: published FAQs grouped by category, server-side
  search, canonical/og meta, JSON-LD FAQPage, escaped org content, brand
  color with luminance-derived ink, header links/CTA/logo, powered-by footer
- Ask AI: rate-limited (rightmost-XFF keyed; Redis SET NX EX + INCR, pruned
  in-memory fallback), concurrency-capped one-shot answer grounded in the
  mapped agent's knowledge (vector search off-loop) + word-overlap-ranked
  published FAQs; short-lived DB sessions around the LLM await so the public
  endpoint can't drain the shared pool; every ask logged for metering
- chat widget embed for the mapped agent's widget
- sitemap.xml / robots.txt / healthz (SSL probe target)

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): custom-domain verification (DNS checks, SSL probe, domain API)

- domain_verification service: TXT (_chattermate.<domain> = cm-verify=token)
  + CNAME checks via dnspython (A/AAAA fallback for CNAME-flattening
  providers, HELP_CENTER_TARGET_IPS), token rotation on re-claim,
  verification regression handling, HTTPS /healthz probe with cert
  validation driving ssl_status
- domain endpoints: claim (409 on taken, 400 on base-domain hosts), verify
  (blocking probes off the event loop), status for UI polling, remove
- tests with mocked DNS/HTTPS covering record combinations and the API flow

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): FAQ manager admin UI

New /faq page mirroring the Knowledge feature structure:
- workspace with empty / generating (4-stage progress) / populated states,
  grouped FAQ cards with inline edit, publish toggle (optimistic), delete
  with confirm, manual add, import-from-URL modal; 3s polling while a job
  is active, full list paged in past the API page cap
- help center appearance: logo upload/remove, brand swatches, header links,
  CTA — debounced autosave with live preview (luminance-contrast ink,
  deliberately theme-exempt: it simulates the external site)
- public section: enable toggle, live URL, AI-search agent selector +
  toggle, custom-domain claim/verify with DNS records table and SSL row
- Pro lock overlay driven by plan_allowed + enterprise module presence
  (OSS never locked); sidebar item + route gated on manage_knowledge
- design-token styling throughout, both themes verified in-browser;
  vitest specs + vue-tsc clean

Verified end-to-end against the dev backend: real Groq generation produced
267 drafts across 33 categories from 68 crawled pages; publish, appearance
autosave, domain claim and the public SSR page (brand recolor, Ask AI
grounded answer, widget embed) all exercised in the browser.

Signed-off-by: chattermate <admin@chattermate.chat>

* docs(help-center): production infrastructure runbook

DNS/wildcard-cert/host-nginx setup for {slug}.chattermate.help, per-customer
custom-domain certificate steps, Cloudflare caveat, enterprise plan-flag
rollout prerequisite and env tunables. Root-level because docs/ is
gitignored (product docs live in the docs repo).

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(knowledge): remove dead OPENAI_API_KEY env write from knowledge search

KnowledgeSearchByAgent exported the org's decrypted API key as a
process-global OPENAI_API_KEY on every construction. The write dates back
to agno's default OpenAI embedder (which errored in prod when the var was
unset); search has since moved to the local FastEmbed embedder and every
model receives its key explicitly, so nothing reads the variable anymore.

Besides being dead, it was a cross-tenant race: concurrent requests for
different orgs overwrote each other's key in shared process state — newly
reachable unauthenticated via the public help-center ask endpoint.

Verified with the backend running with OPENAI_API_KEY explicitly unset:
widget knowledge search and the public Ask AI path work unchanged. The
OPENAI_API_KEY workaround in the prod .env can be dropped after deploy.

Signed-off-by: chattermate <admin@chattermate.chat>

* chore(agent): drop unused OpenAIChat import

app/api/agent.py imported agno's OpenAIChat but never used it (models are
built via create_model). Dead import, removed.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): redesign public site (sidebar, live search, AI card)

Replace the minimal public help-center template with the new design:
sticky header with brand mark, gradient hero + search, a sticky topic
sidebar (icons, counts, "Can't find it?" card), inline AI-answer card
with loading shimmer, per-FAQ feedback, contact CTA and footer.

Kept fully server-rendered and SEO-safe: all published FAQs stay in the
DOM with FAQPage JSON-LD, and every org/FAQ value is Jinja-escaped. The
JS is progressive enhancement only — topic filtering, client-side search
(the GET form is the no-JS fallback), and the explicit Ask-AI fetch
(fires on demand, not per keystroke, to respect the /ask rate limit).
Chat buttons drive the real embedded widget launcher; all chat/AI
affordances gate on widget_id / ask_enabled. Brand tones are derived in
CSS via color-mix, so only --brand/--brand-ink cross from the server.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(help-center): CORS for widget origins + tool-based Ask AI

The chat widget embedded on a help-center host fetches /widgets/{id}/data
and opens a socket from the public origin ({slug}.chattermate.help or a
custom domain), so those origins must be allowed by CORS — otherwise the
widget never loads and chat won't open.

- CORS: add every enabled help-center subdomain + verified custom domain to
  the allowlist (covers socket.io, which can't use a regex), and add an
  allow_origin_regex for *.{base_domain} so a new slug works without a
  refresh. The Redis-sync rebuild path now re-applies the regex too, so a
  refresh can't silently drop the wildcard.
- Refresh CORS when a help center is enabled and when a custom domain
  verifies, propagating to other workers via the existing Redis channel.

Ask AI: give the answer agent real tools (FAQ search + agent knowledge
search) and instruct it to search before answering, instead of pre-stuffing
a single context blob. This fixes over-refusal ("I don't have information on
X") — it now finds and combines answers, and only falls back to "contact
support" after genuinely searching. The tool-using run happens in a worker
thread so the unauthenticated endpoint never holds a DB connection across
the model call.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(help-center): make "Start a chat" wait for the widget launcher

The embedded widget script is async and builds its launcher
(#chattermate-button) a moment after the page loads, so a click on a
"Start a chat" button could land before the launcher existed and silently
do nothing. Poll briefly (~6s) for the launcher and click it as soon as it
appears; warn to the console if the widget never becomes available.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(help-center): load widget loader from FRONTEND_URL + allow dev-port origin

The embedded widget has two parts served from different origins in dev: the
loader (chattermate.min.js) from the frontend, and the widget app it pulls in
(/assets/widget.js) from the backend via VITE_WIDGET_URL. The help-center embed
was building the loader URL from VITE_WIDGET_URL too, so it pointed at the
backend (which doesn't serve the loader). Use FRONTEND_URL for the loader; in
prod both resolve to the app domain.

Also add the dev-port variant of help-center origins to the CORS allowlist:
locally the site is served on :8000 so the browser Origin includes the port,
and socket.io matches origins exactly (no regex), so the widget's socket was
rejected (403). Derived from APP_BASE_URL — no effect in prod (standard port).

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): independent toggles for AI search and chat widget

The AI quick-summary in search and the embedded chat widget were both gated by
ai_search_enabled, so turning off one killed the other. Split them: add a
chat_widget_enabled column (defaults on, backfilled for existing rows) and gate
widget_id_for on it, leaving ask_available on ai_search_enabled. The admin UI
now shows the two toggles under a shared agent selector — pick one, both, or
neither.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): enable/disable toggle for the primary button (CTA)

Add a cta_enabled flag so the header primary button can be turned off without
clearing its text/URL. Defaults on (backfilled for existing rows). The public
template gates both the header CTA and the contact-section fallback on it, the
appearance panel gets a switch that also disables the inputs when off, and the
live preview hides the button to match.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): split FAQ list and settings into tabs

A long FAQ list pushed the help-center settings far down the page. Put the
FAQs (generate bar + list) and the Help center settings (appearance + public)
on separate tabs so neither forces a long scroll. Panes use v-show to keep
polling and autosave state alive across tab switches.

Signed-off-by: chattermate <admin@chattermate.chat>

* refactor(help-center): name the feature 'Help center'; rename settings tab to 'Customization'

The page title and sidebar now read 'Help center' (was 'FAQ' / 'FAQ &
Help Center'). The two tabs become FAQs and Customization so the tab
label no longer collides with the page name.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): rich Markdown articles with dedicated SEO pages + related articles

FAQs become full help-center articles instead of inline accordions:

Public site
- Answers are authored/stored as Markdown and rendered to sanitized HTML
  (markdown -> nh3 allowlist clean) via a new help_center_content service.
- Each published FAQ gets its own server-rendered page at /a/{slug}: rich
  body (headings, images, links, ordered steps, callouts), 'Was this helpful?'
  feedback, and a related-articles grid — matching the new design.
- Landing list cards now link to the article page with a plain-text preview and
  read time; per-article canonical/OG tags, QAPage JSON-LD, and every article
  listed in the sitemap. Shared chrome extracted into base.html.

Data
- Add a per-org-unique 'slug' column (backfill migration add_hc_faq_slug_001);
  slugs assigned once at creation and kept stable so URLs don't churn. Batch
  inserts (generation/import) get slugs at the bulk_create choke point so a
  published generated FAQ never yields a broken link.

Admin
- Markdown-aware answer editor (MarkdownEditor.vue): toolbar (heading/bold/list/
  link), image upload returning a stable absolute URL, and a sandboxed-iframe
  live preview. New POST /help-center/faqs/image endpoint (png/jpeg/gif/webp).

Tests: help_center_content unit tests (render/sanitize/excerpt/read-time).
Signed-off-by: chattermate <admin@chattermate.chat>

* fix(help-center): topic filter — [hidden] must beat author display rules

Author CSS like .hc-card { display:flex } overrides the UA [hidden] rule,
so the landing page's JS topic filter (card.hidden = true) had no visual
effect. Make the hidden attribute authoritative with a global
[hidden] { display:none !important } guard; also protects hc-head,
hc-empty and the Ask-AI cards which toggle via the same attribute.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): Markdown-aware FAQ generation prompts

Answers may now use simple Markdown (steps, lists, bold, in-content
links) since the public renderer supports it; import prompt preserves
the source page's list structure. Groq json-tool schema mirrors the
same guidance.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): size FAQ generation batches to the model's context window

Batches were fixed at 15k chars regardless of model, so big-context
models (Claude 200k, Gemini 1M) paid one LLM call per ~3 pages. New
utils/model_context.py holds a small curated context-window table
(provider floors + model-name prefixes, FAQ_CONTEXT_TOKENS_OVERRIDE
escape hatch) and derives (batch_chars, max_faqs) with prompt-overhead/
output-reserve/safety margins — floor stays at FAQ_MAX_BATCH_CHARS,
ceiling FAQ_MAX_BATCH_CHARS_CEILING (60k) as an extraction-quality
guard. max_faqs scales 15→40 with batch size so yield density is flat,
and max_tokens grows with the reserve.

Signed-off-by: chattermate <admin@chattermate.chat>

* perf(help-center): fuzzy FAQ dedup, 6x smaller prompt question block

The prompt carried up to 200 existing questions per LLM call purely for
dedup. The prompt window is now 30 (intra-run steering only) and the
real guard moved fully into DedupState: exact normalized match plus
token-set Jaccard over an inverted index, with difflib confirmation on
borderline scores, so rephrasings are caught without shipping the
question list to the model. Questions with <3 content words skip fuzzy
matching (too collision-prone).

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): meter FAQ generation against the message limit

Every attempted generation/import LLM call now increments the job's
llm_calls counter (single choke point in draft_batches). Jobs are
stamped metered at enqueue when the org runs the hosted CHATTERMATE
model (FAQ_METER_OWN_KEY env flips to always) — own-key orgs already
pay their provider. New faq_usage service estimates a run's call count
(pages/3 heuristic) and raises 402 before enqueue when a metered run
exceeds the remaining credits; the auto-generation hook skips silently
instead. Enterprise message_limit gains get_remaining_messages and adds
the metered FAQ calls to the period count (submodule — separate PR).

Migration add_hc_job_meter_001: llm_calls, metered, knowledge_ids,
source_file_name on faq_generation_jobs (the latter two feed the
next units: source-targeted regenerate and PDF import).

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): regenerate reads only ungenerated sources

GENERATE_ALL previously re-read every knowledge source each run. It now
skips sources that already have FAQs (distinct faqs.knowledge_id — self-
healing: deleting a source's FAQs makes it eligible again), and the
unwired GenerateRequest.knowledge_ids schema is now accepted by
POST /generate to explicitly re-target specific sources (validated as
org-owned, stored on the job's JSON column). New GET /generate/estimate
returns {total_sources, new_sources, pages, estimated_calls, metered,
remaining_credits} for the pre-generation confirm dialog.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): explicit opt-in before auto-generation + credits confirm

Auto-generation no longer arms itself when the admin page is merely
opened (settings row): the adoption gate now requires a user-initiated
generate/import job or an existing FAQ. The Generate button first shows
a confirm dialog fed by /generate/estimate — new sources, page count,
and (hosted-model orgs) the ~credit cost with remaining balance; the
confirm is blocked client-side when over budget (backend 402 remains
the real guard). Generate bar labels 'Generate N new sources' and
disables when everything is already generated.

Also repairs the FaqCard spec (firebase transitive import + the old
textarea selectors from before the MarkdownEditor swap).

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): bulk delete with multi-select FAQ list

POST /faqs/bulk-delete (org-scoped, foreign ids ignored) + repo
bulk_delete. Frontend gains the app's first multi-select: hover
checkbox per card, per-category select-all (indeterminate), and a
floating bottom toolbar with Publish / Unpublish / Delete (delete via
the shared confirm dialog). Selection clears on refresh.

Test hardening: the help-center API module now bypasses the plan gate
via an autouse fixture (local enterprise builds 403 everything since
the test org has no subscription) with a real_plan_gate opt-out marker
for the gating tests; generate tests updated for the new-source
semantics and estimate endpoint.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): article-mode import — crawl help pages, no LLM

New import mode for real help centers whose index links many article
pages: mode=articles discovers same-site article links (bounded by
FAQ_ARTICLE_IMPORT_MAX_PAGES=30, fragments/queries/binaries/root links
filtered, index-path children preferred, headless-browser fallback for
JS indexes only), fetches each page SSRF-guarded, selects the main
content node (new knowledge/main_content.py mirroring the crawler's
strategies), strips nav/TOC chrome and converts HTML straight to
Markdown via a subclassed markdownify converter that absolutizes links
and re-hosts images (≤10/article, 5MB, type allowlist) through the
shared help_center_images service (moved out of the API layer). One
draft per article: question=title, answer=Markdown, category from
breadcrumb/URL path. No LLM, not metered. MAX_ANSWER_LENGTH raised to
20000 for long articles.

Import modal gains mode radio cards (Q&A extraction vs article crawl).
Also fixes a latent bug: crawl4ai fallback's is_available is a
property — faq_import called it, so the browser fallback never ran.

Verified live against docs.chattermate.chat (link discovery + Markdown
conversion).

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): PDF import — extract Q&A from uploaded documents

POST /import/pdf accepts a PDF (25MB cap, magic-byte validated,
safe_filename), persists it via shared storage so the worker container
can read it back (new load_upload/delete_upload in file_storage +
download_file_from_s3), and enqueues an IMPORT_PDF job. The worker
extracts text page-by-page with pypdf (not agno's PDFReader — that
chunks for the vector KB) and runs the same Q&A extraction/dedup/insert
tail as URL import (refactored into _extract_and_insert); metering
comes free via draft_batches. Import buffers are deleted after the run
(including when enqueue is refused). Clear error for scanned/image
PDFs. Import modal gains the third 'PDF document' mode with file input.

Domain-verification tests get the same env-independent plan-gate bypass
as the API module.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): plain-JSON fallback when structured outputs fail

agno's native response_model path is unreliable on some providers
(Ollama/HuggingFace/Mistral). When the native call raises, or returns
text that isn't the model, non-Groq extraction now retries once with a
strict respond-ONLY-with-JSON instruction and parses the first JSON
object out of the raw text (code fences/prose tolerated). Groq keeps
its dedicated json-tool + salvage path. Hard provider errors still
propagate (the fallback fails the same way).

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(help-center): code-review findings across the generation/import overhaul

- Metering: llm_calls now increments via an on_llm_call hook fired at
  every ACTUAL provider call (wired in build_generator), so the
  plain-JSON fallback's second call is billed — draft_batches'
  per-attempt increment undercounted by up to 2x on fallback-heavy
  providers.
- Article import: page titles are read from <head> BEFORE
  select_main_node strips it (og:title/<title> fallbacks were dead —
  h1-less articles silently dropped); _TOC_RE is word-boundary anchored
  (a class like 'stock-levels' no longer loses its subtree); image
  placeholders carry a .img delimiter so index 1 can't corrupt 10+.
- PDF import: job attrs captured before the try — a DB-originated
  failure no longer raises from the finally (masking the real error and
  leaking the upload buffer); generator built at ANALYZING restoring
  the no-AI-config fail-fast for both import paths.
- Enterprise message_limit: rollback after a failed FAQ-count query so
  the shared chat session isn't left in an aborted transaction
  (submodule — separate PR).
- s3 download offloaded to a thread (was blocking the worker loop).
- Fallback JSON parsing reuses lenient_json_load (truncation-tolerant)
  instead of a naive find/rfind slice.
- Frontend: deletes refresh the generation estimate so the Generate
  button re-enables when a source becomes eligible again; background
  estimate fetches use include_pages=false (2 cheap queries instead of
  N pgvector aggregate scans per page load; the confirm dialog still
  fetches the full estimate).
- Worker notifications say import/imported for import jobs instead of
  generation wording.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(help-center): chunk bulk publish/delete so >200 selection works

The bulk-status/bulk-delete endpoints cap faq_ids at MAX_BULK_IDS (200);
a select-all over an org with more than 200 FAQs sent the whole list and
got 422 Unprocessable. The service now splits ids into 200-id batches
and sums the results, so any selection size works while each payload
stays bounded.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(help-center): reap orphaned FAQ jobs on worker startup

A killed worker (e.g. knowledge_processor stopped mid-job) left the job
in 'processing' forever: /jobs?active=true kept returning it, the UI
polled indefinitely, and the one-active-job-per-type enqueue guard
stayed blocked. The FAQ worker now fails any leftover 'processing' jobs
on startup — it's the sole writer of that status, so on a fresh boot
they can only be orphans.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): searchable, filterable, collapsible FAQ list

Imported articles have long bodies that made every card huge and the
list unscrollable. The admin FAQ list is now compact and navigable:
- Card answer preview is capped (240 chars) and clamped to 2 lines
  (full content stays on the public page and in the editor).
- Search box filters by question/answer text; a topic dropdown filters
  by category; a segmented control filters by All/Published/Draft — all
  client-side over the loaded set, with a result count and a no-results
  state with Clear.
- Category groups are collapsible (chevron per group, published count
  in the header); an active search/filter auto-expands all groups.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(help-center): section-aware article import for help-center portals

Importing a Chatwoot/Zendesk/Intercom-style help center previously
treated category listing pages (e.g. 'Help and support', which lists
many article cards) as a single article — combining every sub-article
into one blob, emitting broken '[### title …](url)' markup, and sweeping
in the 'Made with Chatwoot' footer.

Discovery is now structure-aware: it collects only real /articles/ pages,
tags each with its homepage section heading, and follows /categories/
(and /sections//collections/) listing pages for the full per-category
list — so each article imports individually under its true category
(Help and support, Payments, Integrations…). Curated cross-cut sections
('Featured Articles') don't override a real category. fetch_article
strips help-center chrome: nav, footer, platform branding
(chatwoot.com links), 'Last updated' metadata, and the leading
'Home ›' breadcrumb.

Verified live against help.paywithatoa.co.uk: 50 clean articles across
10 correct categories, no Chatwoot footer/breadcrumb, Markdown headings
and links preserved. MAX_PAGES default 30→50; new
FAQ_ARTICLE_IMPORT_MAX_CATEGORIES caps listing-page follows.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(help-center): clear stuck FAQ jobs from any entrypoint + when worker stays down

The startup reap lived in run_faq_processor_loop, but the actual worker
(run_knowledge_processor) calls run_faq_processor per-iteration and
never hit it — so a killed worker still stranded 'processing' jobs and
the UI polled forever. Fixes:

- run_faq_processor reaps orphans once on its first call (module guard),
  so both worker entrypoints clean up on startup without reaping every
  tick (which would kill the running job).
- Read-side staleness: a 'processing' job whose progress hasn't advanced
  in FAQ_JOB_STALE_SECONDS (default 600s) is excluded from active-job
  polling, so the UI self-clears even if the worker never restarts;
  threshold is generous so a live-but-slow job is never killed.
- Enqueue lazily reaps the org's stale processing jobs first, freeing
  the one-active-per-type guard and partial unique index so a re-run
  isn't blocked by a dead worker's leftover.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(help-center): import complete categories, not a homepage slice

A help-center homepage shows only ~5 of each category's articles, so
importing the homepage captured 5 Devices when the category has 12. Two
fixes: follow the category listing pages BEFORE the homepage section
links, so hitting the page cap yields whole categories rather than a
thin slice of every one; and raise the default page cap to 200 (a
one-time migration in a background worker). Verified against
help.paywithatoa.co.uk: the homepage import now pulls all 100 articles
with correct per-category counts (Payments 21, Features 13, Devices 12…).

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): relevance-ranked search instead of whole-query substring

Search matched the entire query as one contiguous substring, so 'close
my' found nothing while 'how to close' did. All three search surfaces
are now token-based:

- Public page: scored client-side search — every term is matched
  independently (exact title word > title-word prefix > in-title >
  body word-start > body substring), cards matching more terms always
  rank above partial matches, and results are re-ordered by relevance
  (original order restored when browsing). 'close my' now returns
  ranked results; 'refund card' puts 'Voiding and refund card payments'
  first; prefixes like 'termin' match terminal articles. Search runs
  over the answer's plain text (raw Markdown made URLs searchable
  noise) with the title carried separately for boosting.
- Server ?q= fallback (and admin list endpoint): AND of per-term
  ILIKEs across question/answer/category via a shared helper.
- Admin list filter: every term must appear in question/answer/category.

Verified live in the browser against the imported Atoa help center.

Signed-off-by: chattermate <admin@chattermate.chat>

* chore(help-center): drop 'answers may not be perfect' note from AI answer

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): process FAQ jobs one at a time by default

MAX_CONCURRENT_FAQ_JOBS is now an env-configurable setting defaulting to
1 — strictly one business's generation/import at a time (each job does
LLM calls + vector-DB reads, heavy on a small host). Raise the env var
for more throughput on a bigger host.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): themed PDF file picker in the import modal

Replaced the unstyled native file input (browser 'Choose file / No file
chosen') with a themed drop-zone: dashed border, file icon, and a
filled state showing the selected filename + size. Design-token styled
for both themes.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(help-center): PDF import generates FAQs from documents, not verbatim extraction

A PDF is usually documentation/marketing prose (a product guide,
manual), not an existing Q&A list. The verbatim extract_from_faq_page
prompt is told to skip marketing copy, so it produced ~3 FAQs from a
9-page guide. PDF import now uses the generate_from_text prompt (turns
documentation into help-center FAQs) — on the sample CoachIQ guide it
yields 10 well-categorised FAQs per batch instead of 3. URL import
still extracts verbatim (that path migrates existing FAQ pages);
_extract_and_insert gained a mode flag.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): clearer two-step custom-domain setup

The old panel always showed a 'Verify domain' button whose first click
actually saved the domain (generating the records), while the copy said
'add the records below' before any records existed. Reworked into an
explicit flow:
- Empty: enter subdomain → 'Add domain'.
- Pending: shows the domain, numbered steps, and each DNS record as a
  readable card (type badge, Name/Host and Value each with a copy
  button, waiting/detected status) instead of a cramped 4-column table
  that truncated long TXT values — then a 'Verify domain' button with
  an inline SSL note.
- Connected: live-URL confirmation with SSL status.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(help-center): custom brand color via picker + hex input

Beyond the 6 preset swatches, the brand color now has a custom option:
a dashed '+' swatch that opens the OS color picker (and shows the chosen
color, selected, when off-preset), plus a hex text field accepting
3/6/8-digit codes (validated to match the backend, invalid input
reverts with a toast). Both save immediately.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(help-center): code-review fixes for article chrome, PDF drop, color picker

- _strip_chrome no longer over-deletes: breadcrumb removal only fires
  when the article's FIRST link is a back-to-index link (a 'Home' link
  in body prose is never first); the platform-branding link removes only
  a small (<=40 char) footer badge, never a large content div that merely
  links out; and the 'made with/powered by' text pattern is now
  platform-specific so real prose like 'Powered by our AI engine' is
  kept. Verified live: Chatwoot footer/breadcrumb/metadata still stripped.
- PDF import drop-zone advertised 'drop it here' but had no handler, so a
  dropped file navigated the browser away and unloaded the app; added
  dragover/drop handlers with a drag state and PDF-type validation.
- Custom brand color: the native <input type=color> is fed a normalized
  #rrggbb (3-digit expanded, alpha dropped) so opening the picker on a
  3/8-digit color no longer resets it to black.
- Bulk publish/delete: a partially-applied >200-id batch now refetches on
  error so the list can't diverge from the server.

Signed-off-by: chattermate <admin@chattermate.chat>

* test(help-center): fix CI — public-page escaping, content-summarizer mock, CodeQL

- Public page: the index now shows a plain-text preview (Markdown
  stripped) that links to the article page, so the old 'escaped HTML'
  assertion is obsolete; assert the raw tag never appears and add an
  article-page test that the |safe body sanitizes an injected <script>
  while rendering Markdown. Adds an env-independent plan-gate bypass so
  the file runs locally too.
- content_summarizer: drop the patch of knowledge_search_byagent.
  AIConfigRepository — that attribute was removed when the dead
  OPENAI_API_KEY env write was deleted, so the patch raised AttributeError.
- CodeQL (py/incomplete-url-substring-sanitization): the discovery test
  checked 'other-site.com' in url; assert on the parsed hostname instead.

Signed-off-by: chattermate <admin@chattermate.chat>

---------

Signed-off-by: chattermate <admin@chattermate.chat>

v1.0.13

Toggle v1.0.13's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(channels): omni-channel foundation + Telegram (Phase 1) (#187)

* feat(channels): add omni-channel data model

New polymorphic channel tables (channel_accounts with Fernet-encrypted
credentials, channel_conversations mapping external conversations to
sessions, agent_channel_configs for agent routing) plus a persisted
channel column on session_to_agents ('web' default) as the outbound
delivery routing key.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): add channel repositories

ChannelAccountRepository (Fernet-encrypted credential storage),
ChannelConversationRepository (active-session lookup, dispatcher lookup
by session, inbound-window touch), AgentChannelConfigRepository
(per-account agent routing upsert).

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): add ChannelAdapter abstraction and registry

Stateless per-channel adapters behind a uniform interface (webhook
verification, inbound normalization, outbound send, delivery-window
check, markup formatting) with lazy self-registering singletons.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): route replies through outbound delivery dispatcher

deliver_to_customer() sends replies to the session's channel adapter
(widget sessions keep the identical Socket.IO emit). Human replies from
the agent dashboard now reach external channels; failed/window-expired
deliveries are recorded on the message and surfaced to the agent.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): channel-aware ChatAgent and shared lead capture

Add an optional channel kwarg to ChatAgent/create_async (kept separate
from source, which is a knowledge-base filter), extract the widget's
lead-recording block into record_lead_from_response() so external
channels persist leads identically, and parameterize the lead_source
channel previously hardcoded to 'widget'.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): add channel-agnostic message processor

process_channel_message() resolves customer + session (synthetic
channel email, agent from agent_channel_configs), relays to the human
agent's dashboard when a human is handling, otherwise runs ChatAgent
(with MCP cleanup and the enterprise message-limit seam), records
leads, and delivers replies through the outbound dispatcher.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): Telegram channel — adapter, webhook, connect API

TelegramAdapter (secret-token verification, update parsing, sendMessage
with length cap), /webhooks/telegram/{account_id} ingress with Redis
dedupe and background processing, and /channels endpoints to connect a
bot (getMe validation + setWebhook), list accounts, and route an
account to an agent.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): Telegram connect UI and channel badges

Telegram integration card + two-step connect modal (BotFather token,
then agent routing), per-agent bot routing in the agent integrations
tab, and a channel badge in the conversations list and chat header
backed by the new channel field on chat overview/detail APIs.

Signed-off-by: chattermate <admin@chattermate.chat>

* test(channels): Phase 1 omni-channel test coverage

Telegram adapter parsing/verification, outbound dispatcher routing
(web fast-path, adapter path, expired window, missing conversation),
channel processor branches (no-agent drop, AI path, session reuse,
human-handling relay), and connect/webhook API tests. Fixes a
SQLite-visible UUID coercion in the processor's AI-config lookup.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): review fixes — dedupe key, merge isolation, shared cleanup

Scope the Telegram dedupe key by conversation (message_id is only
unique per chat, so messages from different customers collided and were
dropped), isolate the post-merge socket-session repoint so a failure
can't skip the end_chat close, reuse one keep-alive httpx client for
Telegram API calls, extract safe_cleanup_mcp_tools() to replace five
copy-pasted cleanup blocks, and batch the agent-config lookup on the
accounts list (N+1).

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): Meta trio adapters + shared webhook (WhatsApp/Messenger/Instagram)

meta_base.py holds the shared X-Hub-Signature-256 verification, GET
challenge, keep-alive Graph client with retry/backoff, and the 24h
customer-service window check. WhatsApp (window -> template-required,
+ send_template), Messenger, and Instagram (subclasses Messenger)
adapters register on the abstraction. One /webhooks/meta endpoint
verifies the app signature and routes on the payload object field to
the right adapter, with per-conversation dedupe and background
processing. Config: META_APP_ID/SECRET/WEBHOOK_VERIFY_TOKEN/GRAPH_VERSION.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): Meta onboarding API — manual credentials + template send

Connect WhatsApp (phone_number_id + token, Graph-validated, best-effort
WABA subscribe), Messenger (page token validated against page id, page
subscribe), and Instagram (IG id via linked page token). Shared upsert
keeps reconnects credential-fresh and org-isolated. Adds the
send-template endpoint for reopening expired WhatsApp windows and
graph_get/subscribe_app helpers on the shared Meta base.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): Meta connect UI — WhatsApp, Messenger, Instagram cards

One MetaChannelConnect modal parameterized per channel (credential
fields + agent-routing step), integration cards driven by a shared
channel-accounts list with a generic disconnect flow, and the per-agent
routing section generalized from Telegram-only to all messaging
channels.

Signed-off-by: chattermate <admin@chattermate.chat>

* test(channels): Meta trio coverage

Signature verification (incl. unset-secret rejection), GET challenge,
per-channel payload parsing (text/button/status callbacks, echo and
receipt skipping), 24h window semantics per channel (template-required
vs undeliverable, naive-timestamp and never-inbound edge cases),
webhook object-routing + unknown-account ack, connect API validation
paths, org isolation, and template send.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): review fixes — Instagram id guard, token in header

Reject Instagram connects where the Graph-returned id doesn't equal the
entered ig_id (a pasted Page id would validate but never match webhook
entry.id, silently dropping every DM), and move graph_get's access
token from the query string to the Authorization header so long-lived
tokens can't leak into URL/proxy logs.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): surface Telegram setWebhook errors in the connect API

set_webhook now returns Telegram's error description and the connect
endpoint includes it in the 502 (with a BACKEND_URL hint for the
localhost/non-HTTPS case) instead of a generic failure message.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): stop Chrome autofilling the integrations search field

The connect modals' secret inputs were type=password with
autocomplete=off, which Chrome ignores — it treated the page as a login
form and autofilled a saved username into the search box. Switch secret
fields to autocomplete=new-password (respected by Chrome) with unique
names, and make the search field type=search + autocomplete=off.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): Telegram interactive UX — typing, rating, feedback, phone

- Typing indicator (sendChatAction) while the agent composes a reply.
- On end-chat with rating enabled, send a 1-5 star inline keyboard;
  taps arrive as callback_query, recorded as a Rating (once per
  session, acknowledged), then the bot asks for optional written
  feedback captured from the next reply (or /skip).
- When the agent requests contact, show Telegram's native share-phone
  button and store the shared number on the customer.

Adds ChannelInteraction + optional interactive adapter capabilities
(no-ops for other channels), process_channel_interaction, a webhook
interaction-dispatch branch, callback_query in allowed_updates, and
conversation extra-state for the awaiting-feedback flag.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): review fixes for interactive UX

Feedback flag now expires after 30 minutes so a question asked later
reaches the agent instead of being swallowed as rating feedback; rating
capture is guarded by an atomic Redis claim (plus the DB check) against
double-taps, and callback_query redeliveries are deduped by callback
id; forwarded third-party contact cards are rejected — only the
sender's own shared number is stored.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): rewrite Slack onto the unified channel stack

Slack is now a ChannelAdapter like every other channel: signing-secret
webhook verification with replay protection, @mentions keyed per thread
and DMs as continuous conversations, chat.postMessage delivery with
mrkdwn formatting, OAuth v2 install/callback storing the workspace as
an encrypted channel account, and event_id dedupe.

Replaces the legacy integration (plaintext tokens, per-channel configs,
storage modes, slash commands) — legacy tables dropped by migration,
old api/services/repositories/models/tests deleted, frontend Slack card
repointed to the channel-accounts flow and the agent tab's Slack
section folded into the shared messaging-channels list. Human replies
from the dashboard now reach Slack threads via the dispatcher, and
Slack gains MCP tools + lead capture through the shared processor.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): Email, SMS (Twilio) and LINE channels

Email: inbound-parse webhook (SendGrid/Brevo-style payloads, quoted
history stripped), conversations keyed per sender, platform-SMTP
replies threaded via In-Reply-To from conversation state (new
conversation_state adapter hook). SMS: customer-owned Twilio account
with X-Twilio-Signature verification and REST send. LINE: Official
Account with X-Line-Signature verification, push-message replies, and
automatic webhook endpoint registration.

Each has a connect API returning the provider webhook URL, a shared
credential connect modal + integration cards, and agent-tab routing.
Also fixes the Slack review findings: migration downgrade loads the
legacy DDL by file path, and the webhook verifies signatures before
parsing JSON.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): review fixes for Email/SMS/LINE

Email: deterministic sha256 dedupe fallback (hash() is per-process
randomized), removed the over-eager 'From:' quoted-reply marker with a
never-drop fallback, implicit-TLS support for port 465 and graceful
non-STARTTLS servers, autoresponder/bounce detection (Auto-Submitted,
Precedence, X-Auto-Response-Suppress), and a self-addressed guard so a
forward-all inbox can't make the bot answer its own replies. Twilio:
signature verification preserves repeated form keys via multi_items.
LINE: credential decrypt failures fail closed as 403 instead of 500.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): use the real sender email as the customer email

The email channel was appending the synthetic @{channel}.channel suffix
to an address that is already a real email (e.g.
customer@x.com@email.channel), producing malformed records and breaking
customer unification across channels + widget. Use the channel-provided
real email verbatim (lowercased) when present; keep synthesizing for
channels that have no email.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): rating is web-widget only — never asked on channels

Per product decision, ratings are collected on the built-in chat widget
only. Removes the channel-side rating flow entirely: the end-chat rating
prompt, the Telegram star inline keyboard + callback handling, the
follow-up feedback capture (and its awaiting-feedback state, the
claim_once dedupe util, and the unused Rating.update_feedback repo
method). Channel interactions are now contact-only (phone share);
typing indicator and phone-request are unchanged. Adds a UI note under
the agent's Ask-for-Rating toggle clarifying it applies to the built-in
widget only.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): per-inbox outbound SMTP for the email channel

Each connected email inbox can now send replies through its own SMTP
server (stored Fernet-encrypted on the account) so mail goes out from
the customer's domain with correct SPF/DKIM alignment; when omitted it
falls back to the platform SMTP as before. The connect endpoint
validates the SMTP host/credentials with a live login before saving,
and the From header uses the inbox's configured from_email. Frontend
email connect form gains optional SMTP fields.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): make the Manage button work for connected channels

The Manage button on connected integration cards had no handler.
It now opens the channel's modal in 'manage' mode: for Email/SMS/LINE it
skips credential entry and shows the webhook URL (now included in the
accounts list) plus the agent selector; for Telegram/Meta it jumps to
agent routing. Email reconnects no longer wipe stored SMTP credentials
when the SMTP fields are left blank.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): Gmail App Password hint on SMTP auth failure

When validating email SMTP credentials fails with an authentication
error (e.g. 535), surface a clear message: for Gmail/Google hosts,
explain that a 16-char App Password (with 2-Step Verification) is
required instead of the account password; for others, a generic
auth-failed message.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): LINE typing indicator via loading animation

Override send_typing on the LINE adapter to call the Messaging API
loading-animation endpoint (/chat/loading/start) before the agent
replies. The processor already invokes send_typing on every channel, so
LINE now shows a loading indicator like Telegram. Auto-dismisses when
the reply arrives (1:1 chats only).

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): multi-provider SMS (Twilio, Vonage, MessageBird, Plivo, Brevo, AWS SNS)

Refactor the single-vendor Twilio SMS channel into one unified 'sms'
channel with pluggable providers. Each SmsProvider supplies its own
inbound parsing, signature verification, and send API behind a shared
adapter, so conversations, badges and agent routing stay uniform.

Providers: Twilio (X-Twilio-Signature), Vonage/Nexmo (SMS API + optional
MD5 signed webhooks), MessageBird/Bird (AccessKey + optional HMAC
signature), Plivo (X-Plivo-Signature-V3), Brevo (transactional SMS),
AWS SNS (boto3 Publish + SNS HTTPS subscription with cert-based
signature verification and auto-confirmation).

Unified /webhooks/sms/{provider}/{account_id} route and /channels/sms
connect (with a /providers endpoint driving the frontend provider
dropdown + dynamic credential fields). Old sms_twilio adapter and
twilio-specific routes removed; existing connections keep working via
the provider stored in account settings (defaults to twilio).

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): SMS review fixes — SNS cert host + id-less dedupe

Restrict SNS SigningCertURL/SubscribeURL to sns.<region>.amazonaws.com
(the previous *.amazonaws.com check let an attacker host a spoofed cert
on S3 and forge inbound SMS / SSRF via SubscribeURL). Skip webhook
dedupe when a provider gives no message id, instead of hashing the
text — otherwise a customer's legitimate repeated reply ("Yes", "Yes")
was dropped for an hour.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): prompt for agent after Slack connect + in Manage

Slack connects via OAuth redirect (no modal), so unlike the other
channels it never showed the assign-agent step, leaving Slack messages
unrouted. Now the agent picker opens automatically after the Slack
OAuth redirect, and Slack's Manage opens the same picker (instead of
re-running OAuth). Reuses ChannelConnectModal in manage mode for Slack
(agent step only — no credential form).

Signed-off-by: chattermate <admin@chattermate.chat>

* style(channels): themed toggles for agent channel routing + polish disconnect modal

Replace the raw browser checkboxes in the agent editor's messaging-
channels section with themed on/off toggle switches (accent-colored
track, sliding thumb), and lay each account out as a clean row with a
channel badge, name, and active-state highlight — all via design
tokens. Refresh the disconnect confirmation modal: token-based
surfaces/borders/radii, a circular warning badge, tighter typography,
and consistent button styling.

Signed-off-by: chattermate <admin@chattermate.chat>

* feat(channels): Slack real names + typing indicator; tone down channel UI

- Resolve Slack senders to their real name (and email when scoped) via
  users.info instead of showing a raw U0… id; adds a generic
  fetch_profile adapter hook the processor calls when the webhook lacks
  a name, and requests the users:read scope (reconnect Slack to grant).
- Slack typing indicator: post a '_typing…_' placeholder and edit it
  into the reply (Slack has no native bot typing), via a shared
  send_typing/send_text placeholder handshake.
- Reduce the green in the agent editor's channel rows: neutral badges
  and a subtle neutral active-row highlight, keeping the accent only on
  the on/off toggle.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): upgrade placeholder customer name when real name resolves

get_or_create_customer returned the existing customer without touching
its name, so a Slack user already created as 'Slack user U09…' kept
that label even after users.info started returning the real name. Now
a stored '<Channel> user xxxx' placeholder (or empty name) is updated
in place once a real name is available.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): Slack users.info must be form-encoded, not JSON

slack_api sent a JSON body for every method, but Slack's read methods
(users.info) only read args from form-encoded params — so name
resolution returned user_not_found and customers stayed as 'Slack user
U0…'. Add a form flag to slack_api and use it for users.info; writes
(chat.postMessage/update) keep JSON.

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): Vonage HMAC-SHA256 signatures (drop MD5); Meta channels coming-soon

Switch Vonage signed-webhook verification from MD5 to HMAC-SHA256
(resolves the CodeQL weak-hash alert; requires the Vonage signature
method set to SHA-256 HMAC — no prod users on MD5). Document that
Twilio's HMAC-SHA1 is protocol-mandated. Mark WhatsApp / Messenger /
Instagram as 'coming soon' in the integrations UI while Meta app review
is pending (connect machinery stays in place, gated).

Signed-off-by: chattermate <admin@chattermate.chat>

* fix(channels): resolve dangling Slack typing placeholder + gate name enrichment

Code-review follow-ups on the recent channel changes:

- When the AI agent returns None (hard failure), deliver a fallback reply so
  Slack's "_typing…_" placeholder / LINE loading indicator resolves instead of
  dangling as the only message.
- Only call the platform profile API (Slack users.info) while the customer name
  is still a placeholder, via _enrich_customer_name — not on every inbound
  message. Enrichment now runs after customer resolution.
- Prune the in-memory Slack placeholder dict by TTL so a bypassed send_text
  can't leak entries, and delete the stale placeholder when chat.update fails
  so the reply isn't posted beside a leftover "_typing…_".

Tests: placeholder delete-on-update-failure, TTL prune, and enrichment gating.
Signed-off-by: chattermate <admin@chattermate.chat>

---------

Signed-off-by: chattermate <admin@chattermate.chat>

v1.0.12

Toggle v1.0.12's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #159 from chattermate/feature/relicense-apache-2.0

chore(license): relicense open-source core from AGPL-3.0 to Apache-2.0

v1.0.11

Toggle v1.0.11's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(ui): professional redesign + first-run onboarding + light/dark t…

…heme (#152)

* feat(ui): professional Hallmark redesign of design system + app shell

Modern-minimal visual-layer redesign (routes/logic/copy unchanged):
- Tokens: softer terracotta accent (#c2471f), warm-neutral paper/borders,
  Inter (UI) + Montserrat (display), layered shadows, focus-ring token
- Fonts: actually load Inter + Montserrat (were referenced but never loaded)
- Base: display headings, global focus ring, remove stray dark-mode override
- Components: content-sized buttons with full states, bordered inputs with
  focus rings, elevated cards, refined stepper
- Shell: soft-tint active nav with accent bar, white elevated header,
  polished dropdown; fix toggle hover bug and transparent message-banner bug
- Retint Login decorations + Analytics chart colors to new accent

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ui): complete premium redesign across all pages

Finishes the dark-first UI overhaul (TODOs 6-10):
- Conversations/Inbox: dark chat bubbles, sidebar, info panel
- Human Agents: Users/Groups/Roles tabs, role cards, locked overlay
- Settings: Integrations, Widget Apps, User Settings, AI Config
- Common: Modal, NotificationList dark drawer
- Design tokens: full rewrite with lime accent, Space Grotesk/Instrument Sans/JetBrains Mono
- Sidebar, topbar, auth pages, agent editor all updated

* feat(ui): redesign agent list + add topbar page titles

Agent list now matches design exactly:
- Search bar + lime Create Agent button header
- 4-column KPI strip (Active Agents, Conversations, Resolution Rate, Handoffs)
- 2-col agent cards: CSS gradient orbs, name/slug, Online/Web badges, stats row (CHATS/RESOLVED/SOURCES), Configure + Copy widget buttons
- Gradient orbs generated per-agent from name hash (purple/teal, lime/teal, coral/purple palette)
- Search filters agents client-side
- ⋯ card menu button

DashboardLayout: route-based page title in topbar (AI Agents, Human Agents, Inbox, etc.)

* feat(ui): fix widget-apps route title + monospace table headers

- DashboardLayout: corrected route mapping for /settings/widget-apps
- WidgetAppList: table container uses --surface card with 18px radius, th headers in var(--font-mono) 10.5px uppercase, teal Active badge, monospace date cells, hover row highlight

* feat(ui): remove duplicate page titles, upgrade analytics + login styling

- Remove inner page-level h1s from Integrations, Organization, Widget Apps,
  AI Config, and UserList — topbar now owns the page title
- Analytics: upgrade metric cards (surface bg, monospace labels, display font),
  tab underlines → lime accent, time-range pill → lime active button
- Login: add webkit-autofill override so dark input bg is preserved on autofill

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update enterprise submodule pointer (subscription dark mode)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): remove duplicate headings from Groups and Roles tabs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): stage all remaining redesign frontend changes from previous sessions

Includes: design tokens, base CSS, component library, AgentDetail,
AgentInstructionsTab, AIAgentSetup, Modal, Conversations (chat + list +
info panel), AppSidebar, NotificationList, UserSettings, AIAgentView,
ConversationsView, HumanAgentView — all updated to dark-first design system

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(ui): fix all lime-button text color and legacy CSS vars across agent + settings components

- All buttons with accent-ink (lime) bg now use color #0B0C10 (dark text)
- Added missing --background-base and --text-color legacy aliases to design tokens
- Fixed AgentCustomizationView, AgentWorkflowTab, AgentInstructionsTab, AgentAdvancedTab,
  AgentWidgetTab, AgentMCPToolsTab, AgentIntegrationsTab, AgentDetail, AgentChatPreviewPanel
- Replaced hardcoded white modal backgrounds with var(--surface) in KnowledgeGrid, CreateAgentModal, AgentMCPToolsTab
- Fixed old orange rgba(243,70,17) shadows/glows → lime rgba across all agent components
- Updated default accent_color fallback from #f34611 to #C9F24E in AgentDetail
- Fixed WidgetAppList, RoleList radial gradients from orange to lime tint

* feat(ui): purge all remaining orange brand colors from admin UI

Replace legacy #f34611/#c2471f orange with lime #C9F24E across:
- All workflow node config focus rings (10 files)
- Analytics chart colors and radial gradient backgrounds
- Agent customization & preview panel accent_color defaults
- Widget composables (chat_bubble_color/accent_color fallbacks)
- AI setup overlay gradient, chat typing indicator dot

* feat(ui): fix white text on lime buttons + white backgrounds across all remaining components

- AgentChatPreviewPanel: add isColorDark color logic to accentStyles; fix chat-initiation-message bubble bg
- AgentInstructionsTab: upgrade modal header → purple gradient; upgrade button → lime + dark text
- AISetup: upgrade prompt container → purple gradient; locked badge/icon/upgrade button → dark text on lime
- ConversationFilters: filter-toggle-btn.active + apply-btn → dark text on lime
- ConversationsList: load-more-button hover + scroll-to-top-btn → dark text on lime
- ConversationChat: product-card-compact + view-details-button-compact → var(--surface) bg
- FileUpload: drop-zone-content bg → var(--surface)
- Workflow (NodeConfigPanel, PropertiesPanel, WorkflowBuilder, LLMNodeConfig, GuardrailsNodeConfig): all .btn-primary, .add-item-btn, .header-status, .variables-count, .generate-ai-button → dark text on lime
- CustomerAnalytics: view-details-btn → dark text on lime

* feat(ui): extend theme tokens — contrast inks, mode pills, tints, bubbles (dark+light)

Single source of truth for color/font. Adds --on-accent (flips dark→white
between themes so accent-button text stays readable), mode-pill tokens
(blue AI / purple WF), color-mix tints, bubble tokens, brand gradients,
and design-spec radii — to both :root and [data-theme=light].

* feat(ui): app shell to design — collapsed sidebar, topbar, notification drawer

Sidebar: 76px collapsed rail with centered icons, hidden active-bar when
collapsed, 16px section spacers (no divider lines), inline 26px square
toggle, tokenized logo dots. Topbar: 16px/600 title, 40px gradient avatar,
boxed notification button, tokenized danger badge, divider. Notification
drawer: 380px, All/Unread filter tabs, Mark all read, flat rows w/ unread
dot. All token-driven (no hardcoded color/font).

* feat(ui): agent editor header + Agent tab to design

Header: gradient band, 64px avatar ring + lime pencil badge, 26px display
name, 46px teal online toggle, SOLID mode pills (blue AI / purple Workflow)
replacing tinted segmented control, 14px lime-underline tab bar. Agent tab:
Generate-with-AI uses --grad-generate + dark ink; tokenized upgrade modal.
Relaxed over-aggressive <1600/<1440 overrides. Adds --on-dark token.

* fix(ui): inbox 3-way message bubbles to match design

Customer (incoming, left): neutral grey, sharp top-left corner.
AI (outbound, right): teal-tint + teal border, sharp top-right.
Human agent (outbound, right): lime + dark ink, sharp top-right.
Flips sides to match design (customer left / you right — was reversed),
splits the prior 2-way user/bot into a 3-way customer/ai/agent system,
migrates lime-bubble attachment styles, and tints selected list item lime.
All token-driven via --bubble-* tokens.

* refactor(ui): de-hardcode 31 admin components to theme tokens

Replace hardcoded brand hex + font literals across agent, ai, analytics,
workflow, conversations, human-agent, settings, widget-app, jira, org,
user components with design-token vars (--accent-ink, --surface, --text*,
--c-*, --on-accent, --o*, --font-*, --toggle-knob). Dynamic isColorDark
ternaries, JS data defaults, ApexCharts color arrays, and white-on-error
backgrounds intentionally left literal. Single theme file now drives all
admin-page color/font. vue-tsc clean.

* refactor(ui): de-hardcode LoginView auth page to theme tokens

Tokenize all auth-page colors/fonts to design-tokens vars; aurora blobs &
focus glows use color-mix off base palette so they adapt per theme. Login
is now theme-aware (was hardcoded always-dark). Completes no-hardcode rule
for OSS admin pages.

* fix(ui): sidebar logo no longer truncates + even topbar icon spacing

Sidebar: reduce header padding/gaps and drop the ellipsis so 'ChatterMate'
shows fully at 248/252px; bump small-laptop sidebar to 248px.
Topbar: only render the enterprise plan-display when it has content
(loading/trial) — the empty div was injecting a phantom gap between the
theme toggle and the bell, making icon spacing uneven.

* feat(ui): sidebar nav icons match design (inline stroke SVGs)

Replace the <img>+CSS-filter icons with the design's inline stroke icons
(robot, users, inbox, bar-chart, building, card, share-nodes, app-grid,
sliders, person) using stroke=currentColor. Active icons now turn lime
via color inheritance — the filter approach couldn't tint on active.
Drops 10 unused SVG asset imports.

* feat(ui): match all agent-editor tabs to design pixel-by-pixel

Reconcile Chat Customization (style grid, swatches, font chips, greeting
messages), Test/Preview pane + launcher, Knowledge table + add modal,
Integrations cards w/ colored icon badges, Widget embed block, MCP Tools
(cards, create modal, transport radios), Advanced (icon-badge toggle rows
+ number fields), and Workflow Builder (create form, empty state, list
cards) to the design source. Token-driven; all functionality preserved;
vue-tsc clean.

* fix(ui): agent header row + Knowledge 3-col table + Integrations collapse to design

- Header: online toggle + AI Mode + Workflow pills now on ONE row (was
  wrapping to two rows) to match design.
- Knowledge: reduce table to design's 3 columns (SOURCE/TYPE/STATUS) +
  Remove button; drop duplicate wrapper heading + Tips so only the single
  'Knowledge sources' header shows; design empty-state copy.
- Integrations: show simple collapsed Connect cards by default; gate the
  inline enable-toggle + 'not connected' warning behind the connected flag
  (jira/shopify/slackConnected) per design.

* feat(human-agents): redesign to People/Teams/Roles dashboard + team-overview API

Backend: add GET /api/v1/users/team-overview — per-agent active(OPEN)/
resolved(CLOSED) chat counts from SessionToAgent + org KPIs (team size,
online now, live chat load vs capacity, waiting handoff + oldest wait).
Default per-agent capacity 5 (no column yet).

Frontend: rebuild HumanAgentView with design page-header (title, subtitle,
search, Invite agent) + People/Teams/Roles pill tabs with count badges.
- People: KPI strip + agents table (avatar+status dot, Available/Offline
  pill, role badge, team chips, live-load pips, resolved, row menu), wired
  to getTeamOverview() with graceful fallback to listUsers().
- Teams: team cards (online badge, avatar stack, member count, Manage) +
  'Create a team' dashed card.
- Roles: role cards (Default badge, permission chips, Edit) + 'Create a
  custom role' dashed card; premium gating preserved.
All token-driven; CRUD/modals preserved; vue-tsc + py_compile clean.

* feat(ui): match all settings pages to design (Org, User, AI Config, Integrations, Widget Apps)

- Organization: 30px title, stats cards, profile card w/ gradient logo,
  mono domain input, business-hours presets+toggles, sticky blur save bar.
- User Settings: profile card w/ gradient avatar + Upload, mono disabled
  email, Admin role badge, password card, sticky save bar.
- AI Configuration: centered header, in-card tab bar w/ lime underline,
  ACTIVE pill, plan/rate-limit rows, provider card grid, lime CTA.
- Integrations (settings): header + search + N/M-connected summary, card
  grid w/ color-coded icon tiles + Connect/Install/Disconnect/Coming-soon.
- Widget Apps: header + Create app, mono-header table, teal Active badge,
  regen/revoke icon actions, lime-tinted empty state.
All token-driven; CRUD/flows preserved; vue-tsc clean.

* feat(ui): org stats KPI strip + lime send button

Backend: extend GET /organizations/{id}/stats with the design KPIs —
members (total/admins/agents), active_now (online), AI agents (total/
live/draft), conversations 30d + % vs previous 30d.

Frontend:
- Organization: replace 2-stat strip with the design's 4 KPI cards
  (MEMBERS, ACTIVE NOW, AI AGENTS, CONVERSATIONS·30D) wired to the new
  fields with colored subtitles; add Workspace-logo Upload/Remove buttons
  + design subtitle (local preview; persistence pending a backend field).
- ConversationChat: replace the near-invisible dark send <img> with a
  visible lime send button (accent-ink bg, on-accent icon, 38px).
vue-tsc + py_compile clean.

* fix(ui): widget modal, AI config width, password align, collapsed sidebar icons

- Widget create modal: dark tokenized inputs (was white), design
  placeholders, lowercase 'Create widget app' title, Cancel + lime
  'Create app' button order/style per design.
- AI Configuration: remove .provider-info max-width:600 so the plan-table
  and content span the full card width (rows reach the right edge).
- User Settings: stop .form-group + .form-group margin from pushing the
  2nd grid column down — New/Confirm password now align at the top.
- Sidebar: hide nav scrollbar (scrollbar-width:none + webkit) + tighten
  collapsed padding so icons sit centred in the 76px rail (no squeeze).
vue-tsc clean.

* fix(ui): organization full-width cards, danger zone, drop logo upload

- Cards were capped at 480px by the GLOBAL .card rule (max-width:480 +
  margin:0 auto), so the form sat narrow/centered while the stats strip
  spanned 900px. Override in scoped .card (max-width:none, width:100%) so
  Profile/Business-hours/Danger cards align with the stats at full 900px.
- Add Danger zone card (Transfer ownership + Delete organization) per
  design; actions route to support (no destructive API yet).
- Remove the Workspace-logo Upload/Remove buttons + preview logic (no
  backend); logo is display-only again.
- Reduce org-page bottom padding so the footer sits right after content
  instead of hanging far below. vue-tsc clean.

* fix(ui): modal sizing — clean 520px, aligned content, no double padding

Common Modal: replace loose min-width:400/max-width:90% with a balanced
width:100%/max-width:520px, consistent 28px padding, 24px overlay margin
for small screens, tighter header/body spacing. WidgetAppForm: drop its
own padding so fields align with the modal title instead of being inset
(the 'crunched' look). Improves all shared modals (user/group/role/api-key).

* fix(ui): org footer sticks + remove logo; dropdown menu-item button reset

- Organization: drop the nested scroll (.org-settings height:100%/overflow)
  that broke the layout footer — it now sticks to the viewport bottom.
  Remove the Workspace-logo block entirely (+ unused orgInitial/computed).
- Human Agents: .menu-item (row 'Edit Profile' dropdown) was missing
  background/border resets, so the native button chrome showed as a white
  button. Add background:none/border:none/cursor/font — clean dark item.
  Audited all .menu-item rules app-wide; DashboardLayout's was already fine.
vue-tsc clean.

* fix(ui): AI Agents footer sticks + compact Create Role modal

- AIAgentView: drop the nested scroll (.chat-section overflow:auto +
  height:100%) that broke the layout footer — same fix as Organization.
  Footer now sticks to the viewport bottom.
- RoleForm: rebuild styles — permissions in a 2-column grid (was one big
  row each, very tall), tokenized dark inputs + lime checkboxes, remove the
  form's own padding (modal handles it), tokenized Cancel/Create buttons.
vue-tsc clean.

* fix(ui): scroll the content area, not the document — sidebar/topbar stay fixed

The whole document was scrolling, so the sidebar scrolled away and short
pages left blank space below the footer. Pin the layout to the viewport
(.dashboard-layout height:100vh; overflow:hidden) and make .main-content
the scroll container (height:100vh; overflow-y:auto). Now the sidebar +
sticky topbar stay fixed, the inner content scrolls, and the footer sits
at the bottom with no blank space. Drop the forced .content min-height.
Verified: sidebarStaysFixed=true, mainContentScrolls=true, docScroll=0.

* fix(org): consistent admin count + resilient business-hours

- org stats now counts admins by permission (manage_organization/
  super_admin) or role name == Admin, matching the team-overview
  endpoint; a plain Role.name == "Admin" match missed custom/renamed
  roles holding admin perms, inflating the agents count.
- normalize business_hours on load so every day key is present;
  server-stored hours that omit a day no longer crash applyPreset or
  the day-row template.
- drop unused clientTz import in Organizations.vue.

* feat: onboarding wizard, system theme, light-mode accent + agent-editor reskin

Onboarding
- First-run guided setup (Create → Teach → Test → Launch), shown when an org
  has zero agents; AIAgentSetup gates on agent count (skip is session-only and
  returns until the first agent exists).
- Create step is idempotent: creates the agent once, then updates it on return
  (prefilled), avoiding duplicate-agent / plan-limit errors. Type picker with
  per-type instruction prefill, "Generate with AI", and preset/upload avatar.
- Teach reuses useKnowledgeManagement; Test runs a live chat over the public
  widget socket (anonymous token) and surfaces knowledge-indexing status;
  Launch emits the widget snippet + CLI. New composables useOnboardingState /
  useOnboardingTestChat and shared util widgetEmbed (also used by AgentList).
- Backend: stop creating a default Customer Support agent on org creation so
  the wizard actually triggers (organizations.py).

Theme
- Add a System mode that follows prefers-color-scheme and updates live; the
  header toggle cycles Dark → Light → System (useTheme, main.ts, layout).

Accent / light mode
- Split the accent token: --accent-solid (lime, both themes) for filled
  buttons + --on-accent-solid (dark text); --accent-ink stays the readable
  dark-green for accent text on white. Filled accent buttons are now lime in
  both themes instead of a muddy olive; danger buttons keep white text.

Agent editor reskin
- Tokenize AgentDetail + all tabs (Instructions, Customization, Integrations,
  Widget, Advanced, MCP, Workflow, Knowledge) to the design system for light
  and dark; backfill legacy token aliases.
- Avatar picker (12 presets + upload) in the editor header and onboarding;
  kebab menu, ?agent= URL persistence + restore, list re-sync on detail close,
  customization panel height matched to the chat preview.

Auth: LoginView brand panel (Siri orb, badge, copy) reconciled to the design.
vue-tsc clean.

* fix(ui): agent editor polish — flat panel, aligned content, preview width

- Detail is now a single clean white panel (--surface, rounded, 1px border,
  no shadow); header + content share the surface. Removed the leftover
  detail-panel margins and the list's grid padding so the detail fills the
  content area edge-to-edge (standard page gutter only).
- Content cards now align with the tab bar: dropped the content container's
  horizontal padding so each tab's own 24px root sets the inset (tab "Agent"
  and the Instructions card share the same left edge).
- Save Changes footer is flat (hairline + right-aligned button), not an
  isolated card.
- "Generate with AI" buttons + the premium badge use white text on the purple
  gradient (was dark/low-contrast in light mode).
- Test & Preview chat preview: stop flex-grow from stretching the wrapper so it
  honours its 400px (CHATBOT) / 500px (ASK_ANYTHING) width.

* feat(chat): customizable contact collection + premium chat designs

Contact collection
- Pre-chat email is now optional (collect_email, off by default); widget
  connects anonymously when off, gates on a valid email when on.
- On human handoff (incl. no-agents/ticket), the widget shows an inline
  contact form (email + optional name, agent-toggleable) and updates the
  customer record via submit_contact_info / CustomerRepository.update_contact.
- Never surface or promise the anonymous @noemail.com placeholder; transfer
  agent sanitizes it and asks for contact via the form instead.

Premium chat designs
- New chat_style presets: Glass, Terminal, Playful, Calm Mint, Aurora
  (Legacy/Ask-Anything retained); per-theme finish + entrance/reading
  animations; dark-theme-aware bubbles, inputs, forms and dividers.
- Knowledge-base citations end-to-end with a show_citations toggle.
- New lime 3-dot brand mark across favicons, logos, widget launcher.

Migrations: chatstyle enum values, show_citations, collect_email,
handoff_collect_email/name (idempotent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: bump enterprise submodule (collect_email toggle + redesign views)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(citations): default show_citations to false for now

Citations need more work, so ship them off by default. Flips the default
in the migration, model, schema, customization form, and widget computed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: gitignore .hallmark/ (local design-skill logs)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): use absolute-URL check instead of host substring match

CodeQL flagged js/incomplete-url-substring-sanitization where avatar/photo
URLs were classified by `.includes('amazonaws.com')` — a host substring
that can appear anywhere in an attacker-influenced string. Replace with an
`isAbsoluteUrl()` helper (absolute http(s) scheme test) in avatars.ts and
apply it in AgentList, UserList, and useWidgetStyles. Rebuild widget bundle.

Also remove the one-off gen-favicons script and its sharp/png-to-ico devDeps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): replace remaining amazonaws.com host-substring checks

Resolve the rest of the CodeQL js/incomplete-url-substring-sanitization
findings — including the two in the built widget.js bundle (from WidgetBuilder
and useWidgetFiles) — by routing all 'is this an absolute S3/CDN URL' checks
through isAbsoluteUrl(). useWidgetFiles now derives the S3 object key from the
parsed URL pathname instead of splitting on the host substring. Rebuild bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: update view specs to redesigned markup

The professional redesign changed DOM/classes that these unit specs assert on
(pre-existing breakage on this branch, unrelated to the main merge):
- LoginView: .login-container→.auth-page, .error-message→.auth-error, 'Signing in...'→'Signing in…'
- HumanAgentView: .tab-nav→.tab-pills, .tab-btn→.tab-pill, Users/Groups/Roles→People/Teams/Roles
- AIConfigSettings: page header/<h1> moved to the topbar; drop the obsolete header assertions

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(enterprise): bump backend submodule for zero-agent onboarding signup

Points the enterprise_backend submodule at main (7a92d34), which moves default
agent creation to Shopify-only so regular signups start with no agents — the
onboarding wizard this redesign introduces relies on that behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(crawl4ai): skip live network/browser tests in CI

The two integration tests do a real network fetch + launch real Chromium. On CI
(Python 3.12) fetch_with_browser's event-loop teardown leaves a Crawl4AIFallback
coroutine un-awaited and raises 'Event loop is closed' during GC, which then fails
an unrelated test in the same run (non-deterministic blame).

Gate both behind a CI guard: skipped when CI is set unless RUN_LIVE_CRAWL_TESTS=1,
so they still run locally on demand. Verified locally: CI=true => 18 passed, 2 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(crawl4ai): cover real threaded event-loop path with mocks

Skipping the live integration tests dropped total coverage just under the 65%
gate. Add two mocked tests that exercise the real run_in_new_loop thread + new
event loop path in _run_async_safely (success + exception), which the existing
Thread-mocked test never executed. No network/browser — _async_fetch is mocked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: chattermate <arun@paywithatoa.co.uk>

v1.0.10

Toggle v1.0.10's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(analytics): sentiment analytics dashboard + conversation deep-li…

…nk (#149)

* feat(analytics): sentiment analytics dashboard + conversation deep-link

Surfaces the sentiment data captured by the backend (PR #142) in the UI:

- New SentimentAnalytics.vue: distribution donut, avg-sentiment trend
  (y -1..1), summary cards, and a "Sessions Needing Attention" table.
- Wire a "Sentiment" tab into AnalyticsDashboard (apexchart already
  registered globally; reuses the existing time-range selector).
- Deep-link from the negative-sessions table to the conversation history:
  ConversationsView reads ?session=&status= and passes initialSessionId to
  ConversationsList, which fetches the chat detail by id (works for closed
  sessions not in the current list) and pre-selects the matching tab.
- Fix get_time_range_dates() to return timezone-aware UTC bounds. Naive
  utcnow() bounds were interpreted in the DB session timezone for the
  timestamptz columns, silently dropping recent rows when that tz wasn't
  UTC (e.g. analytics showing no data for messages created "now").

* test(conversations): mock vue-router in ConversationsView spec

ConversationsView now calls useRoute() for deep-link query params, so the
test must provide a vue-router mock (useRoute returned undefined → "Cannot
read properties of undefined (reading 'query')").

v1.0.9

Toggle v1.0.9's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #135 from chattermate/pr-133

Pr 133

v1.0.8

Toggle v1.0.8's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #124 from chattermate/slack_integration

Slack integration

v1.0.7

Toggle v1.0.7's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #114 from chattermate/pr/cdmx-in/113-2

Pr/cdmx in/113 2

v1.0.6

Toggle v1.0.6's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #71 from chattermate/feature/inboxrevamp

Feature/inboxrevamp