[
    {
        "id": "019e0cba-3f15-718b-b6a9-8cabd6ade8a3",
        "version": "v1.1.0",
        "title": "Visual workflow editor, customizable lead form, and widget polish",
        "body": "## New features\n\n**Visual workflow editor.** Build branching chat flows on a\ndrag-and-drop canvas. Add trigger nodes, message bubbles, ask-and-\ncapture questions, conditional branches, lead tags, and outbound\nwebhooks. Branches fan out visually and reconnect cleanly. If you\nprefer a keyboard-first view, every workflow round-trips to a\nlinear form editor — same data, two ways to edit it.\n\n**Conditional logic in workflows.** Three new step types let your\nflows do real work: branch on a captured answer (equals, contains,\nstarts-with, is-empty, default), tag the lead so your CRM can\nroute it, and POST conversation state to any external URL. Match\nmodes any / all / exact let keyword triggers fire on the right\nintent instead of every loose match.\n\n**Custom lead-form builder.** Each agent now has its own lead-form\nschema. Pick from text, email, phone, long-text, dropdown, and\ncheckbox fields. Reorder them, mark which are required, set\nplaceholders and length limits. Four presets ship out of the box\n(Classic, B2B SaaS, Support, GDPR-friendly). The same schema\nrenders in both the inline mid-conversation form and the new\npre-chat gate so visitors see a consistent form everywhere.\n\n**Pre-chat lead gate.** Optional per-agent toggle. When on,\nvisitors see your lead form before the chat surface unlocks —\nthe conversion pattern Intercom and Drift have used for a decade.\nOnce submitted, the chat opens on the same panel with no reload.\nReturning visitors don't see the gate again.\n\n**Widget position picker.** Three placement options for every\nagent: centered bar across the bottom (the default), floating\nbubble in the bottom-right corner (the classic Intercom / Drift\nlayout), or bottom-left (mirrored, useful when the right edge of\nthe page is busy with other widgets).\n\n**Restricted paths.** Per-agent list of URL patterns where the\nwidget should not appear. Drop in `/admin/*`, `/checkout`, or\n`/account/*` and the widget stays out of those flows without\ntouching your site code.\n\n**Annual billing.** Pricing page now has a monthly / annual\ntoggle that surfaces the savings percentage based on each plan's\nconfiguration. Stripe Checkout uses the matching price.\n\n**PayPal and Razorpay payment gateways.** Stripe is no longer the\nonly checkout option. PayPal works in every market PayPal serves\n(huge for buyers outside the US/EU); Razorpay covers India + South\nAsia with native support for UPI, cards, and net-banking. Both\ngateways slot into the existing plan + subscription model — admins\ntoggle each gateway on or off from Settings → System and visitors\npick at checkout. Webhook signature verification on both ends; the\ncaptured-lead and receipt emails inherit the same white-label\ncascade as Stripe.\n\n**Per-plan AI controls.** Rate limits and max-tokens are now\nconfigurable per plan from the admin console, so you can match\neach tier's generosity to your actual cost.\n\n**Live lead notifications.** A toast appears in the dashboard the\nmoment a lead lands. Workspace members can opt in to browser-push\nnotifications too, so the alert reaches them even when the\ndashboard tab is in the background.\n\n**Marketing site testimonials and FAQ.** The homepage gains an\nauto-advancing testimonial carousel and a 10-question FAQ\naccordion. Both are editable from Settings → Marketing — replace\nthe shipped defaults with your own quotes and questions.\n\n**Search-engine optimization.** Every public page now ships\nproper SEO out of the box: per-page title and description,\ncanonical URL, Open Graph card for social sharing, Twitter\nsummary card, and structured data (Organization, SoftwareApplication,\nFAQPage). New `/sitemap.xml` lists every public route plus your\npublished changelog versions. Updated `/robots.txt` allows\ncrawlers on public pages and blocks them from admin / API\nsurfaces.\n\n**White-label cascade everywhere.** Your brand name and logo now\nflow through outbound billing emails (Stripe, PayPal, Razorpay\nreceipts), the captured-lead email, and the widget's \"Powered by\"\nfooter attribution.\n\n**Lead-email pipeline test.** Settings → System → Mail gains a\n\"Send test lead email\" button that sends the actual lead-captured\nnotification through your queue worker — catches the missing-\nworker bug class instantly. The existing raw SMTP test stays for\nchecking mail-server connectivity.\n\n**Public marketing site toggle.** New \"Public marketing site\"\nswitch in Settings → Branding. Turn it off and unauthenticated\nvisitors hitting `/`, `/pricing`, `/changelog`,\n`/documentation`, etc. are redirected to `/login` — useful for\nprivate / internal SaaS deployments. `/privacy`, `/terms`, and the\nauth flows stay accessible always; search engines are told to\nskip the entire domain.\n\n## Improvements\n\n**Workflows page redesigned.** The workflows list now matches the\nrest of the admin: search bar, status filter, sort menu, column\nvisibility toggle, and pagination. Same shape as Agents, Leads,\nand Conversations.\n\n**Customizable widget defaults.** Three-tier merge for widget\nappearance: shipped defaults → platform admin defaults → workspace\noverrides → per-agent. Set a sensible house style once; customers\noverride only what they want.\n\n## Fixes\n\n**Widget no longer appears on signed-in admin or customer pages.**\nA two-layer fix: the application's root layout suppresses the\ndemo widget for authenticated users (server-side, can't be\nmisconfigured), and the new Restricted Paths feature gives buyers\nfine-grained per-page control.\n\n**Mobile homepage hero text no longer overflows** on narrow\nviewports (especially iPhone SE).\n\n**Settings → Widget** is back to a single layout wrap with a\ntwo-column form and a live preview that matches the real omnibar.\n\n**Brand consistency on the dashboard.** The \"Set up Pitchbar in a\nfew steps\" sidebar card and the widget's demo pill no longer\nhard-code the source brand name — both follow your install's\nconfigured site title.\n\n**\"What's new\" banner.** Fixed a layout bug where the leading\n\"What's new in\" text could be clipped behind the sidebar on wide\nscreens. Banner is now mounted inside the content column so the\nsidebar can't overlap it.\n\n**Changelog survives `migrate:fresh`.** Release notes now persist\nto `storage/app/private/changelog-entries.json` instead of a\ndatabase table, and auto-seed from version-controlled markdown\nfiles at `database/changelog-entries/v*.md` on first read. New\ndeploys pick up shipped release notes automatically; existing\nentries are never overwritten.\n",
        "status": "published",
        "released_at": "2026-05-09T00:00:00+00:00",
        "created_by_user_id": null,
        "created_at": "2026-05-09T12:33:12+00:00",
        "updated_at": "2026-05-09T13:10:33+00:00",
        "source": "markdown"
    },
    {
        "id": "019e1ae6-e890-70b1-8e33-b1fbd1df3edf",
        "version": "v1.3.0",
        "title": "WordPress & WooCommerce plugin, BYOK, free file parsing, hardened security",
        "body": "## New: official WordPress & WooCommerce plugin\n\nA first-party WordPress + WooCommerce plugin now ships with every\nPitchbar deployment. Drop it on any WordPress 6.4+ install running\nPHP 7.4 or newer, paste a workspace API token, and the streaming\nchat widget loads on every public page — with content sync, product\ncards, coupon application, order lookup, and lead mirroring out of\nthe box.\n\n**Widget embed.** One asynchronous `<script>` tag injected into\n`wp_footer`, scoped to the post types you opt into. Hidden on\n`wp-admin`, login, AJAX, REST, XML-RPC, and cron paths. Emits\n`data-page-dir` and `data-page-locale` attributes so the bar\nmirrors correctly on Arabic, Hebrew, Persian, and Urdu locales.\n\n**Content sync.** Bulk + delta ingest of posts, pages, custom post\ntypes, and WooCommerce products as Pitchbar knowledge sources. SHA-\n256 content hash short-circuits re-indexing on unchanged content,\nso re-running \"Sync now\" against a stable site is effectively free.\n\n**Page-builder rendering.** Elementor, Beaver Builder, Oxygen, and\nBricks pages are rendered with the builder's own native API before\nsync, so the synced HTML reflects what visitors actually see. Divi\nposts route through `the_content` with `setup_postdata` primed so\nthird-party filter callbacks see a valid `$post` global. A\n`pitchbar_post_content_html` filter lets sites post-process the\nfinal HTML (strip nav chrome, force a custom template, redact a\nsection) without forking the plugin.\n\n**Resumable sync.** Each sync pass enforces a 20-second wall-clock\nbudget. When a large site can't finish in one tick, the plugin\npersists a resume marker as a WordPress transient and schedules a\nWP-Cron continuation 30 seconds out. Shared hosting with a 30s\n`max_execution_time` cap no longer fatals mid-sync. The Plugins\nadmin shows a soft notice while a chunked sync is in flight.\n\n**WooCommerce shopper context.** When a logged-in WC customer\nvisits a page, the plugin attaches a signed `data-shopper-token`\nto the widget script tag. The widget forwards it to `/widget/init`;\nPitchbar verifies it and bakes `wp_user_id` + `email_hash` (never\nplaintext) claims into the chat session.\n\n**Order lookup tool.** EcommercePreset's `order_status` capability\nmaps to a real tool. When the visitor asks about orders, the LLM\ncalls `lookup_order(limit, order_number?)`, which POSTs to\n`/wp-json/pitchbar/v1/orders/lookup` over HMAC-signed callback.\nReturns id, number, status, total, currency, items, tracking URL\n(AfterShip / ST tracking / generic meta), and the customer's order\nview URL.\n\n**Coupon emission & apply.** The plugin's `CouponSyncer` snapshots\npublish-status `shop_coupon` entries after every product sync (max\n50), filters expired / over-limit, and posts them to Pitchbar. The\nLLM emits `<coupon code=\"WELCOME10\" .../>` markers in chat replies;\nthe widget renders them as cards with Copy and Apply buttons. Apply\nstages the code in a 15-minute transient that auto-applies on the\nvisitor's next cart load via the `woocommerce_load_cart_from_session`\nhook.\n\n**Abandoned cart re-engagement.** A new `BehaviorRule.kind =\n\"abandoned_cart\"` lets you re-engage a visitor whose cart has been\nidle. The plugin's `cart-state.js` mirrors every WooCommerce\n`added_to_cart` / `removed_from_cart` event into `localStorage`; the\nwidget polls every 30 seconds and fires when the cart sits past\nthe rule's `idle_minutes` threshold.\n\n**Lead push-back.** Every Pitchbar lead is mirrored to WordPress as\na WC customer (when Woo is active) or WP subscriber, with\n`pitchbar_lead_id` + `pitchbar_conversation_id` in user meta so the\nstore owner can correlate. Idempotent on email — re-pushes update,\nnever duplicate.\n\n**Workspace API tokens.** New `/settings/api-tokens` admin page\nissues workspace-scoped bearer tokens with granular abilities (the\nplugin uses `wp:integration`). Pitchbar stores only the SHA-256\nhash of the plaintext; revoke instantly invalidates without\ndeleting audit history. Cross-workspace revoke is forbidden.\n\n**HMAC signed callbacks.** Pitchbar → plugin calls (order lookup,\ncoupon apply, lead push) are signed with HMAC-SHA256 over the raw\nbody using a per-token `shopper_signing_secret`. 5-minute replay\nwindow. The plugin verifies via constant-time `hash_equals`.\n\n**Super-admin plugin distribution.** New\n`/admin/integrations/wordpress` page lets super-admins build and\ndownload the install-ready zip directly from the Pitchbar admin.\nBacked by the new `php artisan pitchbar:build-wp-plugin` Artisan\ncommand.\n\n## Improvements\n\n**Plugin-load-order race fixed.** v1.x of the plugin trusted\n`class_exists('WooCommerce')` at `plugins_loaded` priority 10,\nwhich silently disabled WooCommerce features when alphabetical\nplugin order put `pitchbar` before `woocommerce`. v2.0.0 defers\nevery WC-dependent registration to the `woocommerce_loaded` action\n(with a `did_action` fallback for the priority-20+ case), so the\nrace is eliminated.\n\n**Coupon enumeration via `shop_coupon` CPT.** Replaces the v1.x\nreliance on `wc_get_coupons()` (not public WC API on every\nrelease) with `get_posts(['post_type' => 'shop_coupon'])` +\n`new WC_Coupon($id)`. Works on every WC version since 2.0.\n\n**`abandoned_cart` admin trigger.** Backend already supported it;\nthe agent Behavior triggers admin page now exposes it in the kind\ndropdown.\n\n**Comprehensive WordPress docs.** Seven new documentation pages\nunder the \"WordPress & WooCommerce\" group: Overview, Install &\nconnect, Content sync, Page builders, WooCommerce deep links,\nREST API reference, Troubleshooting.\n\n## Fixes\n\n**Page-builder content reaches the RAG store.** Pre-v2.0 the\nplugin called `apply_filters('the_content', $post->post_content)`\nwithout priming `setup_postdata`, which made plenty of\n`the_content` callbacks early-return on `is_null($post)`. The\nElementor / Bricks / Oxygen path was worse — those builders don't\nstore layout in `post_content` at all, so the synced HTML was\nempty. The fix detects each builder via postmeta and calls its\nnative renderer.\n\n**Sync survives shared hosting timeouts.** The 30-second PHP\nexecution cap on shared hosts no longer fatals mid-sync on large\ncatalogs. The resumable sync pattern (transient + WP-Cron\ncontinuation) lets a 5,000-post sync finish over multiple ticks.\n\n**Conversation cookie lifecycle.** The widget writes\n`pitchbar_conv_id` on init success so the plugin's\n`CartCouponController::applyPendingCoupon` hook can locate the\ncorrect staged coupon. Pre-v2.0 the cookie was only set when\n`?pitchbar_conv` arrived as a query parameter, which left\nchat→cart flows without a correlation key.\n\n## New: BYOK (Bring Your Own Keys)\n\nEach workspace can now pay its own Cloudflare / OpenAI / OpenRouter /\nQdrant bill. Super-admin flips a global toggle in\nSettings → System; customers paste their keys at Settings → AI keys.\nPer-user override (force-on / force-deny / inherit-global) lets you\ngrant or block individual users independent of the global flag.\n\nStrategic for operators reselling Pitchbar as a SaaS — the more\nend-customers scale, the less you owe upstream. Workspace-level\ncredentials are encrypted at rest via `APP_KEY`\n(`workspaces.byok_keys` is an `encrypted:array` cast). Five layers\nof tenant isolation guarantee workspace A's keys never leak to\nworkspace B's container resolution. See `/documentation/byok` for\nthe full matrix.\n\n## New: file parsing routed through Cloudflare's free `toMarkdown`\n\nPDF, DOCX, DOC, XLSX, XLS, ODT, ODS uploads now flow through\nCloudflare Workers AI's `toMarkdown` endpoint instead of the\nin-process Smalot / PhpWord parsers. Cost: zero (toMarkdown for\ndocument formats consumes 0 Neurons). Quality: structured markdown\nout — headings, lists, tables preserved, which feeds the chunker\nmuch better than Smalot's best-effort plain text on Word-exported\nor scanned-with-text-layer PDFs. CSV / MD / TXT stay on the local\nparsers; HTML / SVG uploads are rejected at the validator. Falls\nback to Smalot when Cloudflare credentials are absent (BYOK OpenAI\ncustomers, fresh installs) or when the Cloudflare call fails.\n\n## New: sitemap fan-out + 500-page cap\n\nThe default `services.crawl.max_pages_per_source` bumped from 25 →\n500. Buyers adding a 100-URL sitemap silently lost 75 pages.\n`SitemapDiscoverer` now handles three input shapes correctly:\ndomain root (probes `/sitemap.xml` + `/sitemap_index.xml`), direct\nsitemap URL (fetched verbatim — previous code appended a second\n`/sitemap.xml`), and `<sitemapindex>` recursion. CMSes that emit a\nsitemap-index by default (WordPress, Shopify, Webflow) get every\npage indexed instead of just the homepage. Override the cap via\n`CRAWL_MAX_PAGES_PER_SOURCE` in env.\n\n## New: always-visible \"Talk to a human\" button\n\nThe widget header carries the button at all times for any agent\nwhose vertical includes the `ticket_escalation` capability (every\npreset ships with it enabled by default). Plus\n`HumanIntentDetector` matches ~40 intent phrases (\"talk to a\nhuman\", \"connect me to an agent\", \"I need to speak with someone\",\n…) in chat messages and short-circuits the LLM straight into the\nhandoff flow.\n\n## New: 2-minute wait for human handoff + notifications\n\nClicking \"Talk to a human\" used to fall straight to \"no one's\naround\" when no operator was actively monitoring the dashboard.\nNow: the conversation ALWAYS queues and notifies, the visitor\nwaits up to 2 minutes (`WAIT_TIMEOUT_SECONDS = 120`), and every\nworkspace member with `live_chat_available=true` gets a database\n+ mail notification (`HumanRequestedNotification`). Operators who\nhad the dashboard closed still see the queued conversation in the\nsidebar on next sign-in.\n\n## New: public KB articles\n\nCurated answers can now be published as public knowledge-base\npages at `/kb/{workspace.slug}/{article.slug}`. Each curated\nanswer row carries `kb_title` + `kb_published` toggle and an\ninline publish pill in the admin grid. KB pages render\noperator-supplied markdown through a hardened converter\n(`App\\Support\\SafeMarkdown`) — raw HTML stripped, `javascript:` /\n`vbscript:` / `data:` URIs stripped from links.\n\n## New: Google Sheets as a knowledge source\n\nPick a sheet tab, the crawler ingests every row and treats each\nas a chunk seed. Mirrors the Notion / Google Docs OAuth path.\n\n## New: multi-currency + lifetime-deal pricing\n\nVisitors on the marketing pricing page can pick their currency;\nStripe Checkout uses the matching price. Lifetime-deal plans\nrender as a separate section under the subscription tiers.\n\n## Security hardening\n\n**Stored XSS on public KB pages — blocked.**\n`Illuminate\\Support\\Str::markdown` defaults to `html_input='allow'`\nand `allow_unsafe_links=true`, which would make `<img onerror=...>`\nand `[click](javascript:...)` in a KB article execute as live JS\non every visitor browser. New `App\\Support\\SafeMarkdown` helper\nswaps in `html_input='strip'` + `allow_unsafe_links=false`. Ten\nunit tests pin every payload.\n\n**SSRF guard on the crawler.** A workspace admin pasting\n`http://169.254.169.254/...` (AWS metadata),\n`http://localhost:6379/` (loopback Redis), or\n`http://10.0.0.5/admin` (intranet) used to walk straight into the\nserver's internal network on the `PlainHttpCrawler` fallback. New\n`App\\Support\\UrlSafetyGuard` blocks every private / loopback /\nlink-local CIDR, refuses non-http(s) schemes, and (on crawl jobs)\nDNS-resolves hostnames to catch rebind attacks where\n`attacker.com` points at `127.0.0.1`. `PlainHttpCrawler` also\nre-validates every redirect hop and disables auto-follow.\n\n**Widget Origin re-check on every privileged endpoint.** New\n`VerifyWidgetOrigin` middleware re-validates the request Origin\nagainst the JWT-bound agent's `allowed_origins` on every\n`/v1/widget/*` endpoint except `init` (which enforces inline\nbefore issuing the JWT). Defence-in-depth against stolen JWTs\nreplayed from an unlisted origin.\n\n**Browser-level defence headers on every web response.** CSP\n(`default-src 'self'; object-src 'none'; base-uri 'self';\nform-action 'self'; frame-ancestors 'self'`), HSTS on HTTPS\nrequests (1 year + subdomains), `X-Content-Type-Options: nosniff`,\n`Referrer-Policy: strict-origin-when-cross-origin`. Scoped to the\n`web` middleware group — the widget API is excluded because\nbuyers embed cross-origin.\n\n**Upload MIME allowlist.** `UploadController` requires\n`mimes:pdf,docx,doc,xlsx,xls,csv,md,markdown,txt,odt,ods` on\nevery uploaded file. `.html` / `.exe` / `.svg` / `.zip` uploads\nget a 422 at the validator before the parser sees a byte.\n\n**Mass-assignment cleanup on `User`.** `byok_enabled` and\n`default_workspace_id` removed from `User::#[Fillable]`. Both are\nprivilege-bearing — sanctioned admin paths use `forceFill` after\nauthorisation.\n\n## Improvements\n\n**Crawler retry policy.** `CrawlPageJob` now distinguishes\nrate-limited (release-with-fresh-backoff, no retry slot burned),\npermanent (DNS error, 4xx) — short-circuit via `fail()` — and\ntransient (5xx) failures. Stops the `MaxAttemptsExceededException`\nflood that used to fill production logs whenever a customer\nindexed a domain with a few dead URLs. Per-job timeout 90s,\n`failOnTimeout=true` so timeouts still flip Source rows to\n`failed` with customer-readable errors.\n\n**Cloudflare Worker cron driver hardened.** `TickCommand`\n`--max-time` bumped 25 → 55s (the loop exits before the 60s tick\nboundary), `--job-timeout` default 120s, queues processed:\n`crawl,index,default`. Resolves the\n`MaxAttemptsExceededException` flood reported in production.\n\n**PDF reindex works for uploads.** `UploadController` now\npersists parsed segment text under\n`storage/app/private/uploads/{source_id}/segment-N.txt`. The\nReindex button reads it back without re-uploading the original.\nPre-fix, \"Reindex\" was a no-op on file uploads because the\ncontroller short-circuited on a null `document.url`.\n\n**PDF parser sanitization.** Strips PDF stream artifacts, control\nbytes, and \"/Type /Page\" leakage that some encoders dump into\n`getText()`. Falls back to full-document text when per-page\nextraction returns empty (common for Word-exported PDFs). Drops\nsegments with <75% printable characters as \"no extractable text\"\ninstead of indexing garbage.\n\n**`pages_total` in source preview reads\n`services.crawl.max_pages_per_source`.** The progress indicator\non the Sources preview now reflects the current cap (500 by\ndefault) instead of the hard-coded 25 that was stale after the\nbump.\n\n**Cloudflare AI Gateway support.** Set\n`CLOUDFLARE_AI_GATEWAY_URL` to route Workers AI calls through\nCloudflare's gateway — observability, caching, optional\nper-account rate limits.\n\n**Documentation refresh.** New `/documentation/byok` page;\nsecurity, knowledge, live-chat, allowed-origins, env, and\ndeployment pages all updated to reflect the v1.3.0 changes.\n\n## Fixes (operator-reported during v1.3.0)\n\n**\"AI keys\" link surfaced to customers.** After a buyer enrolled\nBYOK from the admin dashboard, their customers had no way to find\nthe `/settings/byok-keys` form because no nav entry pointed at it.\nFixed by adding a conditional sidebar entry driven by the\n`byok.unlocked` Inertia shared prop.\n\n**\"AI keys\" link hidden from super_admin.** Super-admins were\nseeing the customer-facing BYOK menu. They manage platform-wide\nAI credentials at `/settings/system` instead; the workspace BYOK\nform is customer-only. The controller hard-404s for super_admin\ntoo as belt-and-braces.\n\n**Token-count cap precision.** Replaced naive `mb_strlen / 4`\nheuristics for the max-tokens budget with a Unicode-aware\nestimator that distinguishes ASCII (4 chars/token) from CJK\n(~1 char/token). The plan's `max_tokens` ceiling now applies\ncorrectly to non-English visitor messages.\n\n**Sources errors are sanitised before reaching the customer UI.**\nRaw Cloudflare 401 JSON envelopes used to bleed through to the\nSources list. `SourceErrorPresenter` now rewrites them to short\nfriendly lines.\n",
        "status": "published",
        "released_at": "2026-05-13T00:00:00+00:00",
        "source": "markdown",
        "created_by_user_id": null,
        "created_at": "2026-05-12T06:36:40+00:00",
        "updated_at": "2026-05-13T16:52:46+00:00"
    },
    {
        "id": "019e2080-3800-71a4-bc73-6d32d3a3a136",
        "version": "v1.2.0",
        "title": "Smarter follow-ups, multi-CTA cards, and behavior-trigger reliability",
        "body": "## New features\n\n**Suggested follow-up questions.** After every answer, the widget\nnow shows up to three follow-up prompts the visitor can tap to keep\nthe conversation moving. Reduces the dead-end \"I don't know what\nelse to ask\" drop-off. Generated automatically from the answer's\ncontext — no setup needed.\n\n**Stage-aware typing indicator.** Instead of a generic \"...\" dot,\nthe widget now tells the visitor exactly what's happening:\n\"Searching your site…\" while we pull relevant pages, then\n\"Thinking…\" while the AI writes the reply, then the streamed\nanswer. Makes the wait feel deliberate instead of frozen.\n\n**Unread badge + soft chime when minimised.** If a visitor walks\naway from a half-finished reply, the widget pops a small red badge\non the closed bar and plays a one-time soft tone. They come back,\nsee \"1 new message\", click, and pick up where they left off. The\nchime respects the system's \"reduce motion\" preference and never\nplays more than once per session.\n\n**Up to three CTA cards stack per reply.** Earlier the agent could\nonly show a single call-to-action card after an answer (\"Book a\ndemo\"). Now configure up to three CTAs with priorities — all\nmatching ones render as a clean stack so a visitor asking about\npricing sees Pricing, Demo, and Docs together. Pre-fix only the\ntop-priority CTA shipped.\n\n**Lead-form trigger strategy.** Every agent now has four ways to\ndecide when the inline lead form appears: <strong>Engagement</strong>\n(default — fires once intent is detected or after a few turns),\n<strong>First message</strong> (every visitor sees the form on\nturn one — best for sales-led agents), <strong>Keyword only</strong>\n(only when the visitor signals real intent like \"pricing\" /\n\"demo\"), and <strong>Never</strong> (chat without lead capture).\nPick the one that matches your funnel.\n\n**Self-host installation guide.** New <code>/documentation/installation</code>\npage walks operators from a fresh server to a running Pitchbar\ndeployment — requirements, environment variables, database setup,\nasset build, Octane/Horizon/Reverb processes, cron, first admin,\nsmoke test, troubleshooting. Covers both host cron and the\none-click Cloudflare Cron Worker option.\n\n**Simplified Chinese for the WordPress plugin.** The Pitchbar\nWordPress companion plugin (v2.0.5) now ships with a complete\nSimplified Chinese (zh_CN) translation pack — 53 strings covering\nsettings, sync buttons, status messages, and admin notices.\nWordPress automatically loads it when the site or user locale is\nset to 简体中文.\n\n**Bundled standalone documentation in the WordPress plugin.**\nEvery plugin zip now includes a single-file <code>documentation.html</code>\nbuyers can open without an internet connection. Mintlify-style\nreference covering installation, content sync, page builders,\nWooCommerce, REST API, troubleshooting.\n\n**Connected WordPress sites surface per agent.** The Integrations\npage in the dashboard now shows every WordPress site that\nconnected the companion plugin to one of your agents — site URL,\nplugin version, WooCommerce status, \"Last seen X ago\". Confirms\nthe integration is live without having to log into each WP install.\n\n## Improvements\n\n**Existing agents no longer answer \"I don't have enough\ninformation\" with sources indexed.** The default similarity\nthreshold for newly created agents drops from 0.78 to 0.5 to match\nhow Cloudflare's embedding model scores real matches (it scores\nlower than OpenAI's by design). Visitors now get answers from the\ncontent you indexed instead of the fallback line.\n<strong>Note for existing agents:</strong> agents created before\nthis release may still be on 0.78. If you see the \"no information\"\nfallback, open the agent's settings and lower the threshold to\n0.5.\n\n**Analytics page survives stale-schema deploys.** Earlier a single\nbroken column on the analytics dashboard would return a generic\n\"Internal Server Error\" page. The dashboard now degrades\ngracefully — missing data shows as zeros, a yellow banner explains\n\"some metrics couldn't be loaded — the most likely cause is a\npending database migration\", and the rest of the page renders\nnormally.\n\n**Customer-safe wording on crawl errors.** When a crawl failed —\nservice hiccup, missing page, blocked site — the Sources list\nused to show the raw upstream message (sometimes including\nJSON envelopes from Cloudflare with status codes and error bodies).\nThe customer-facing column now shows short friendly lines like\n\"We couldn't reach this page\" or \"The crawl service is busy right\nnow — we will retry automatically.\" Operators still see the full\nraw message under <strong>Show details</strong>.\n\n**PDF, TXT, DOCX uploads now reliably index.** A queue worker\nconfiguration was missing the indexing lane for file uploads, so\nPDFs and text documents could sit in \"pending\" indefinitely on\nsome deployments. Knowledge uploads from now on flow through\ncorrectly and finish indexing within a minute.\n\n## Fixes\n\n**Behavior triggers now actually fire.** The widget's behavior\ntriggers — exit-intent, idle, scroll-depth, time-on-page,\nabandoned-cart — were silently disabled in earlier releases. Rules\nconfigured in the admin saved fine and showed up in the live\ninit payload, but a teardown bug detached the listeners\nmilliseconds after they attached, so they never ran. Buyer\nDovydas's \"behavior triggers don't work\" report turned out to be\nreal and a critical regression. Fixed; every trigger kind now\nfires as documented. No reconfiguration needed.\n\n**Three configured CTAs no longer collapse to one.** A buyer\nreported that adding three CTAs to an agent only ever rendered the\ntop-priority one. The selector now returns up to three matching\nCTAs and the widget stacks all of them. Existing agents already\nconfigured with multiple CTAs see them all without any setup\nchange.\n\n**No raw Cloudflare 401 JSON leaks into the Sources list.**\nAuto-indexed sources from visitor pages would sometimes show the\nraw upstream error envelope (Cloudflare Browser Rendering JSON\nwith status codes, error codes, and authentication messages)\ndirectly in the customer-facing column. The customer now sees a\nsanitized \"the crawl service is temporarily unavailable on your\nworkspace\" message instead; the raw message stays available under\nthe operator's Show details toggle.\n\n**Analytics dashboard no longer 500s on stale schema.** Companion\nto the Improvements section above — the actual cause of every\n\"Internal Server Error\" report on <code>/app/analytics</code> we'd\nseen in the last few weeks.\n",
        "status": "published",
        "released_at": "2026-05-13T00:00:00+00:00",
        "source": "markdown",
        "created_by_user_id": null,
        "created_at": "2026-05-13T08:42:14+00:00",
        "updated_at": "2026-05-13T08:42:14+00:00"
    },
    {
        "id": "019e71f5-6b1a-7173-84c6-911f767aade6",
        "version": "v2.0.0",
        "title": "WordPress & WooCommerce plugin, BYOK, free file parsing, hardened security",
        "body": "## New: official WordPress & WooCommerce plugin\n\nA first-party WordPress + WooCommerce plugin now ships with every\nPitchbar deployment. Drop it on any WordPress 6.4+ install running\nPHP 7.4 or newer, paste a workspace API token, and the streaming\nchat widget loads on every public page — with content sync, product\ncards, coupon application, order lookup, and lead mirroring out of\nthe box.\n\n**Widget embed.** One asynchronous `<script>` tag injected into\n`wp_footer`, scoped to the post types you opt into. Hidden on\n`wp-admin`, login, AJAX, REST, XML-RPC, and cron paths. Emits\n`data-page-dir` and `data-page-locale` attributes so the bar\nmirrors correctly on Arabic, Hebrew, Persian, and Urdu locales.\n\n**Content sync.** Bulk + delta ingest of posts, pages, custom post\ntypes, and WooCommerce products as Pitchbar knowledge sources. SHA-\n256 content hash short-circuits re-indexing on unchanged content,\nso re-running \"Sync now\" against a stable site is effectively free.\n\n**Page-builder rendering.** Elementor, Beaver Builder, Oxygen, and\nBricks pages are rendered with the builder's own native API before\nsync, so the synced HTML reflects what visitors actually see. Divi\nposts route through `the_content` with `setup_postdata` primed so\nthird-party filter callbacks see a valid `$post` global. A\n`pitchbar_post_content_html` filter lets sites post-process the\nfinal HTML (strip nav chrome, force a custom template, redact a\nsection) without forking the plugin.\n\n**Resumable sync.** Each sync pass enforces a 20-second wall-clock\nbudget. When a large site can't finish in one tick, the plugin\npersists a resume marker as a WordPress transient and schedules a\nWP-Cron continuation 30 seconds out. Shared hosting with a 30s\n`max_execution_time` cap no longer fatals mid-sync. The Plugins\nadmin shows a soft notice while a chunked sync is in flight.\n\n**WooCommerce shopper context.** When a logged-in WC customer\nvisits a page, the plugin attaches a signed `data-shopper-token`\nto the widget script tag. The widget forwards it to `/widget/init`;\nPitchbar verifies it and bakes `wp_user_id` + `email_hash` (never\nplaintext) claims into the chat session.\n\n**Order lookup tool.** EcommercePreset's `order_status` capability\nmaps to a real tool. When the visitor asks about orders, the LLM\ncalls `lookup_order(limit, order_number?)`, which POSTs to\n`/wp-json/pitchbar/v1/orders/lookup` over HMAC-signed callback.\nReturns id, number, status, total, currency, items, tracking URL\n(AfterShip / ST tracking / generic meta), and the customer's order\nview URL.\n\n**Coupon emission & apply.** The plugin's `CouponSyncer` snapshots\npublish-status `shop_coupon` entries after every product sync (max\n50), filters expired / over-limit, and posts them to Pitchbar. The\nLLM emits `<coupon code=\"WELCOME10\" .../>` markers in chat replies;\nthe widget renders them as cards with Copy and Apply buttons. Apply\nstages the code in a 15-minute transient that auto-applies on the\nvisitor's next cart load via the `woocommerce_load_cart_from_session`\nhook.\n\n**Abandoned cart re-engagement.** A new `BehaviorRule.kind =\n\"abandoned_cart\"` lets you re-engage a visitor whose cart has been\nidle. The plugin's `cart-state.js` mirrors every WooCommerce\n`added_to_cart` / `removed_from_cart` event into `localStorage`; the\nwidget polls every 30 seconds and fires when the cart sits past\nthe rule's `idle_minutes` threshold.\n\n**Lead push-back.** Every Pitchbar lead is mirrored to WordPress as\na WC customer (when Woo is active) or WP subscriber, with\n`pitchbar_lead_id` + `pitchbar_conversation_id` in user meta so the\nstore owner can correlate. Idempotent on email — re-pushes update,\nnever duplicate.\n\n**Workspace API tokens.** New `/settings/api-tokens` admin page\nissues workspace-scoped bearer tokens with granular abilities (the\nplugin uses `wp:integration`). Pitchbar stores only the SHA-256\nhash of the plaintext; revoke instantly invalidates without\ndeleting audit history. Cross-workspace revoke is forbidden.\n\n**HMAC signed callbacks.** Pitchbar → plugin calls (order lookup,\ncoupon apply, lead push) are signed with HMAC-SHA256 over the raw\nbody using a per-token `shopper_signing_secret`. 5-minute replay\nwindow. The plugin verifies via constant-time `hash_equals`.\n\n**Super-admin plugin distribution.** New\n`/admin/integrations/wordpress` page lets super-admins build and\ndownload the install-ready zip directly from the Pitchbar admin.\nBacked by the new `php artisan pitchbar:build-wp-plugin` Artisan\ncommand.\n\n## Improvements\n\n**Plugin-load-order race fixed.** v1.x of the plugin trusted\n`class_exists('WooCommerce')` at `plugins_loaded` priority 10,\nwhich silently disabled WooCommerce features when alphabetical\nplugin order put `pitchbar` before `woocommerce`. v2.0.0 defers\nevery WC-dependent registration to the `woocommerce_loaded` action\n(with a `did_action` fallback for the priority-20+ case), so the\nrace is eliminated.\n\n**Coupon enumeration via `shop_coupon` CPT.** Replaces the v1.x\nreliance on `wc_get_coupons()` (not public WC API on every\nrelease) with `get_posts(['post_type' => 'shop_coupon'])` +\n`new WC_Coupon($id)`. Works on every WC version since 2.0.\n\n**`abandoned_cart` admin trigger.** Backend already supported it;\nthe agent Behavior triggers admin page now exposes it in the kind\ndropdown.\n\n**Comprehensive WordPress docs.** Seven new documentation pages\nunder the \"WordPress & WooCommerce\" group: Overview, Install &\nconnect, Content sync, Page builders, WooCommerce deep links,\nREST API reference, Troubleshooting.\n\n## Fixes\n\n**Page-builder content reaches the RAG store.** Pre-v2.0 the\nplugin called `apply_filters('the_content', $post->post_content)`\nwithout priming `setup_postdata`, which made plenty of\n`the_content` callbacks early-return on `is_null($post)`. The\nElementor / Bricks / Oxygen path was worse — those builders don't\nstore layout in `post_content` at all, so the synced HTML was\nempty. The fix detects each builder via postmeta and calls its\nnative renderer.\n\n**Sync survives shared hosting timeouts.** The 30-second PHP\nexecution cap on shared hosts no longer fatals mid-sync on large\ncatalogs. The resumable sync pattern (transient + WP-Cron\ncontinuation) lets a 5,000-post sync finish over multiple ticks.\n\n**Conversation cookie lifecycle.** The widget writes\n`pitchbar_conv_id` on init success so the plugin's\n`CartCouponController::applyPendingCoupon` hook can locate the\ncorrect staged coupon. Pre-v2.0 the cookie was only set when\n`?pitchbar_conv` arrived as a query parameter, which left\nchat→cart flows without a correlation key.\n\n## New: BYOK (Bring Your Own Keys)\n\nEach workspace can now pay its own Cloudflare / OpenAI / OpenRouter /\nQdrant bill. Super-admin flips a global toggle in\nSettings → System; customers paste their keys at Settings → AI keys.\nPer-user override (force-on / force-deny / inherit-global) lets you\ngrant or block individual users independent of the global flag.\n\nStrategic for operators reselling Pitchbar as a SaaS — the more\nend-customers scale, the less you owe upstream. Workspace-level\ncredentials are encrypted at rest via `APP_KEY`\n(`workspaces.byok_keys` is an `encrypted:array` cast). Five layers\nof tenant isolation guarantee workspace A's keys never leak to\nworkspace B's container resolution. See `/documentation/byok` for\nthe full matrix.\n\n## New: file parsing routed through Cloudflare's free `toMarkdown`\n\nPDF, DOCX, DOC, XLSX, XLS, ODT, ODS uploads now flow through\nCloudflare Workers AI's `toMarkdown` endpoint instead of the\nin-process Smalot / PhpWord parsers. Cost: zero (toMarkdown for\ndocument formats consumes 0 Neurons). Quality: structured markdown\nout — headings, lists, tables preserved, which feeds the chunker\nmuch better than Smalot's best-effort plain text on Word-exported\nor scanned-with-text-layer PDFs. CSV / MD / TXT stay on the local\nparsers; HTML / SVG uploads are rejected at the validator. Falls\nback to Smalot when Cloudflare credentials are absent (BYOK OpenAI\ncustomers, fresh installs) or when the Cloudflare call fails.\n\n## New: sitemap fan-out + 500-page cap\n\nThe default `services.crawl.max_pages_per_source` bumped from 25 →\n500. Buyers adding a 100-URL sitemap silently lost 75 pages.\n`SitemapDiscoverer` now handles three input shapes correctly:\ndomain root (probes `/sitemap.xml` + `/sitemap_index.xml`), direct\nsitemap URL (fetched verbatim — previous code appended a second\n`/sitemap.xml`), and `<sitemapindex>` recursion. CMSes that emit a\nsitemap-index by default (WordPress, Shopify, Webflow) get every\npage indexed instead of just the homepage. Override the cap via\n`CRAWL_MAX_PAGES_PER_SOURCE` in env.\n\n## New: always-visible \"Talk to a human\" button\n\nThe widget header carries the button at all times for any agent\nwhose vertical includes the `ticket_escalation` capability (every\npreset ships with it enabled by default). Plus\n`HumanIntentDetector` matches ~40 intent phrases (\"talk to a\nhuman\", \"connect me to an agent\", \"I need to speak with someone\",\n…) in chat messages and short-circuits the LLM straight into the\nhandoff flow.\n\n## New: 2-minute wait for human handoff + notifications\n\nClicking \"Talk to a human\" used to fall straight to \"no one's\naround\" when no operator was actively monitoring the dashboard.\nNow: the conversation ALWAYS queues and notifies, the visitor\nwaits up to 2 minutes (`WAIT_TIMEOUT_SECONDS = 120`), and every\nworkspace member with `live_chat_available=true` gets a database\n+ mail notification (`HumanRequestedNotification`). Operators who\nhad the dashboard closed still see the queued conversation in the\nsidebar on next sign-in.\n\n## New: public KB articles\n\nCurated answers can now be published as public knowledge-base\npages at `/kb/{workspace.slug}/{article.slug}`. Each curated\nanswer row carries `kb_title` + `kb_published` toggle and an\ninline publish pill in the admin grid. KB pages render\noperator-supplied markdown through a hardened converter\n(`App\\Support\\SafeMarkdown`) — raw HTML stripped, `javascript:` /\n`vbscript:` / `data:` URIs stripped from links.\n\n## New: Google Sheets as a knowledge source\n\nPick a sheet tab, the crawler ingests every row and treats each\nas a chunk seed. Mirrors the Notion / Google Docs OAuth path.\n\n## New: multi-currency + lifetime-deal pricing\n\nVisitors on the marketing pricing page can pick their currency;\nStripe Checkout uses the matching price. Lifetime-deal plans\nrender as a separate section under the subscription tiers.\n\n## Security hardening\n\n**Stored XSS on public KB pages — blocked.**\n`Illuminate\\Support\\Str::markdown` defaults to `html_input='allow'`\nand `allow_unsafe_links=true`, which would make `<img onerror=...>`\nand `[click](javascript:...)` in a KB article execute as live JS\non every visitor browser. New `App\\Support\\SafeMarkdown` helper\nswaps in `html_input='strip'` + `allow_unsafe_links=false`. Ten\nunit tests pin every payload.\n\n**SSRF guard on the crawler.** A workspace admin pasting\n`http://169.254.169.254/...` (AWS metadata),\n`http://localhost:6379/` (loopback Redis), or\n`http://10.0.0.5/admin` (intranet) used to walk straight into the\nserver's internal network on the `PlainHttpCrawler` fallback. New\n`App\\Support\\UrlSafetyGuard` blocks every private / loopback /\nlink-local CIDR, refuses non-http(s) schemes, and (on crawl jobs)\nDNS-resolves hostnames to catch rebind attacks where\n`attacker.com` points at `127.0.0.1`. `PlainHttpCrawler` also\nre-validates every redirect hop and disables auto-follow.\n\n**Widget Origin re-check on every privileged endpoint.** New\n`VerifyWidgetOrigin` middleware re-validates the request Origin\nagainst the JWT-bound agent's `allowed_origins` on every\n`/v1/widget/*` endpoint except `init` (which enforces inline\nbefore issuing the JWT). Defence-in-depth against stolen JWTs\nreplayed from an unlisted origin.\n\n**Browser-level defence headers on every web response.** CSP\n(`default-src 'self'; object-src 'none'; base-uri 'self';\nform-action 'self'; frame-ancestors 'self'`), HSTS on HTTPS\nrequests (1 year + subdomains), `X-Content-Type-Options: nosniff`,\n`Referrer-Policy: strict-origin-when-cross-origin`. Scoped to the\n`web` middleware group — the widget API is excluded because\nbuyers embed cross-origin.\n\n**Upload MIME allowlist.** `UploadController` requires\n`mimes:pdf,docx,doc,xlsx,xls,csv,md,markdown,txt,odt,ods` on\nevery uploaded file. `.html` / `.exe` / `.svg` / `.zip` uploads\nget a 422 at the validator before the parser sees a byte.\n\n**Mass-assignment cleanup on `User`.** `byok_enabled` and\n`default_workspace_id` removed from `User::#[Fillable]`. Both are\nprivilege-bearing — sanctioned admin paths use `forceFill` after\nauthorisation.\n\n## Improvements\n\n**Crawler retry policy.** `CrawlPageJob` now distinguishes\nrate-limited (release-with-fresh-backoff, no retry slot burned),\npermanent (DNS error, 4xx) — short-circuit via `fail()` — and\ntransient (5xx) failures. Stops the `MaxAttemptsExceededException`\nflood that used to fill production logs whenever a customer\nindexed a domain with a few dead URLs. Per-job timeout 90s,\n`failOnTimeout=true` so timeouts still flip Source rows to\n`failed` with customer-readable errors.\n\n**Cloudflare Worker cron driver hardened.** `TickCommand`\n`--max-time` bumped 25 → 55s (the loop exits before the 60s tick\nboundary), `--job-timeout` default 120s, queues processed:\n`crawl,index,default`. Resolves the\n`MaxAttemptsExceededException` flood reported in production.\n\n**PDF reindex works for uploads.** `UploadController` now\npersists parsed segment text under\n`storage/app/private/uploads/{source_id}/segment-N.txt`. The\nReindex button reads it back without re-uploading the original.\nPre-fix, \"Reindex\" was a no-op on file uploads because the\ncontroller short-circuited on a null `document.url`.\n\n**PDF parser sanitization.** Strips PDF stream artifacts, control\nbytes, and \"/Type /Page\" leakage that some encoders dump into\n`getText()`. Falls back to full-document text when per-page\nextraction returns empty (common for Word-exported PDFs). Drops\nsegments with <75% printable characters as \"no extractable text\"\ninstead of indexing garbage.\n\n**`pages_total` in source preview reads\n`services.crawl.max_pages_per_source`.** The progress indicator\non the Sources preview now reflects the current cap (500 by\ndefault) instead of the hard-coded 25 that was stale after the\nbump.\n\n**Cloudflare AI Gateway support.** Set\n`CLOUDFLARE_AI_GATEWAY_URL` to route Workers AI calls through\nCloudflare's gateway — observability, caching, optional\nper-account rate limits.\n\n**Documentation refresh.** New `/documentation/byok` page;\nsecurity, knowledge, live-chat, allowed-origins, env, and\ndeployment pages all updated to reflect the v2.0.0 changes.\n\n## Fixes (operator-reported during v2.0.0)\n\n**\"AI keys\" link surfaced to customers.** After a buyer enrolled\nBYOK from the admin dashboard, their customers had no way to find\nthe `/settings/byok-keys` form because no nav entry pointed at it.\nFixed by adding a conditional sidebar entry driven by the\n`byok.unlocked` Inertia shared prop.\n\n**\"AI keys\" link hidden from super_admin.** Super-admins were\nseeing the customer-facing BYOK menu. They manage platform-wide\nAI credentials at `/settings/system` instead; the workspace BYOK\nform is customer-only. The controller hard-404s for super_admin\ntoo as belt-and-braces.\n\n**Token-count cap precision.** Replaced naive `mb_strlen / 4`\nheuristics for the max-tokens budget with a Unicode-aware\nestimator that distinguishes ASCII (4 chars/token) from CJK\n(~1 char/token). The plan's `max_tokens` ceiling now applies\ncorrectly to non-English visitor messages.\n\n**Sources errors are sanitised before reaching the customer UI.**\nRaw Cloudflare 401 JSON envelopes used to bleed through to the\nSources list. `SourceErrorPresenter` now rewrites them to short\nfriendly lines.\n",
        "status": "published",
        "released_at": "2026-05-21T00:00:00+00:00",
        "source": "markdown",
        "created_by_user_id": null,
        "created_at": "2026-05-29T04:19:29+00:00",
        "updated_at": "2026-05-29T04:19:29+00:00"
    }
]