Changelog
2026-03-26 (Domain validation denial logging)
Added
- Structured logs for domain 403s —
validateDomainForSite()returns areason(missing_origin_referer,empty_site_key,site_not_found,hostname_not_in_site_domains,database_error)./track/config,/forms/config, and browser-mode/lead.captureemitdomain_validation_denied(warn) withreason,request_hostname,route, andsite_key./track/configuses the request logger so denials appear even whenLOG_TRACKINGis off.
2026-03-21 (Rate limit migration renumbered to 040)
Changed
- D1 migration file —
RateLimitCountsDDL moved frommigrations/039_rate_limit_counts.sqltomigrations/040_rate_limit_counts.sqlso numbering aligns with039_sites_record_session.sql. Apply scripts (apply-local-migrations.sh,apply-production-migrations.sh) list040_rate_limit_counts.sqlafter039_sites_record_session.sql. Production installs that already ran the old039_rate_limit_countsfile should skip or verify idempotently (CREATE TABLE IF NOT EXISTS).
2026-03-16 (Refresh button cooldown separate from cache TTL)
Added
- Refresh button cooldown (separate setting) — Refresh button cooldown (seconds) in Settings → CRM & dashboard controls only how long the Refresh button is disabled after each click (default 1800 = 30 min, max 86400 = 1 day). Analytics cache TTL continues to control how long data is cached in KV (default 7 days). So you can keep cache at 7–30 days to reduce load while limiting Refresh misuse to 30 min or 1 hour. Backend:
analytics_refresh_cooldown_secondsin AppSettings;X-Refresh-Cooldown-Secondsresponse header on cacheable analytics GETs;getRefreshCooldownSec()insrc/tracking/analyticsCache.ts. Dashboard uses this header for the cooldown timer; falls back to cache TTL if header is absent.
Changed
- Analytics cache TTL default 7 days — Default for Analytics cache TTL (seconds) is 604800 (7 days). Settings UI allows up to 2592000 (30 days). Cache TTL no longer controls the Refresh button; use Refresh button cooldown for that.
- Refresh cooldown message — Dashboard, Lead stats, and Moderation cache banners show a human-friendly cooldown (e.g. "Refresh available in 30 minutes") via
formatRefreshCooldown()indashboard/src/lib/utils.ts.
2026-03-15 (Marketing API: Dropdown options visible by default)
Fixed
- Marketing API users not seeing Dropdown options — "Dropdown options" was in the admin-only list and off by default for new marketing keys, so the sidebar hid it and marketing users didn’t see the item. Dropdown options is now a marketing page: it’s included in the default visible pages for new keys (dashboard and backend
DEFAULT_MARKETING_VISIBLE_PAGES). New marketing keys get the Dropdown options sidebar item and page access by default. Existing keys are unchanged; admins can edit a key in Settings → Marketing API keys and enable Dropdown options under Marketing pages if needed. - 403 Forbidden on
/analytics/dropdown-optionsfor marketing — The analytics route blocked all marketing users from the dropdown-options endpoint. It now checks permission: marketing can accessGET/POST /analytics/dropdown-optionsandGET/PATCH/DELETE /analytics/dropdown-options/:idwhen their key has/dropdown-optionsin its visible pages (canAccessDropdownOptions). Admins retain full access.
2026-03-15 (Per-site session recording control)
Added
- Per-site rrweb recording control — Session (DOM) recording can be turned on or off per site. When you embed the tracker with
PulseGate.init({ siteKey, ... }), the tracker fetchesGET /track/config?siteKey=...(domain-validated) and only starts rrweb when the site has Record session enabled. Sessions and behavioral events are still created and stored; only rrweb recording is conditional. Dashboard: Forms config → expand a site → Record session (replay) toggle. Backend:Sites.RecordSession(migration 039, default 1),GET /track/config, tracker script checks config before callingstartRrwebRecording(). Embeds withoutsiteKeykeep previous behaviour (recording always on).
2026-03-15 (IP whitelist/blocklist mutual exclusivity)
Changed
- IP cannot be both whitelisted and blocked — An IP must be either on the whitelist or the block list, not both. Blocking an IP that is whitelisted (or whitelisting an IP that is blocked) now returns 400 with a clear message: remove it from the other list first. Backend:
blockIpchecksisIpWhitelistedandaddWhitelistedIpchecksisIpBlocked; analytics route returns 400 when the service returns anerrorfield. Dashboard Blocked IPs and Whitelisted IPs pages show the validation error inline when the API returns 400.
2026-03-15 (Dropdown options as separate route for marketing)
Added
- Dropdown options as a separate route — Dropdown options (Source, Country, Project, City) are now on a dedicated route
/dropdown-optionsinstead of only under Settings. Admins can grant marketing team access to this page via Settings → Marketing API keys by enabling Dropdown options when creating or editing a key. Sidebar shows Dropdown options under Settings for users who have access. Old paths/settings/dropdown-optionsand/location-project-configredirect to/dropdown-options.
2026-03-15 (D1 read reduction: AppSettings cache + lead drought KV)
Changed
- D1 read reduction — To cut down high D1 read volume (e.g. ~4M/day with little traffic), two optimizations are in place: (1) AppSettings cache:
getAppSettings()is cached in KV for 60 seconds (pg:app_settings). Every analytics request (auth + handler) previously did 1–2 full D1 reads; now they hit KV first. Cache is invalidated when settings are saved viaPATCH /analytics/settings. (2) Lead drought cron (already merged): when "Enable lead drought alert" is off, the cron only reads the enabled flag from KV and skips D1 entirely. Seedocs/operations/d1-read-sources.mdfor where D1 reads come from and how to investigate high usage.
2026-03-15 (D1 SELECT queries and index coverage)
Added
- docs/operations/d1-select-queries-and-indexes.md — Audit of SELECT queries used in the app vs existing D1 indexes. Lists which queries are covered by indexes and which are not; recommends new indexes for Leads (ContactEmail, ContactIP, CreatedAt, optional composites), LeadSubmissions (ContactIP), sessions (last_seen), fingerprints (vpn_suspected), and BlockedIPs/WhitelistedIPs (BlockedAt/WhitelistedAt for ORDER BY). Notes that
schema.sqlhasidx_contact_ipon Leads but migrations do not.
2026-03-12 (Marketing page access: route alignment and persistence)
Fixed
- Marketing user page assignments not saving / always showing defaults — Root cause: (1) Settings page options did not include all dashboard routes (
/lead-stats,/moderationwere missing), so admins could not assign them and saved keys had an incomplete list. (2) Layout only updated marketing visible pages whenmarketing_visible_pages.length > 0, so when the API returned a small or empty list the dashboard kept stale defaults. (3) Default page lists differed between dashboard and backend (e.g./ai,/ai/askmissing in backend default). Changes:MARKETING_PAGE_OPTIONSin Settings now matches sidebar routes (added Lead stats, Moderation; order aligned). Layout now updates from banner whenevermarketing_visible_pagesis an array (not only when length > 0). Dashboard and backendDEFAULT_MARKETING_VISIBLE_PAGESaligned (include/ai,/ai/ask). Marketing users should refresh after an admin changes their key’s pages to see the new list.
2026-03-12 (On-demand refresh + cache banner)
Added
- On-demand refresh and cache banner — Dashboard, Lead stats, and Moderation pages now show a banner when analytics cache is configured: "Data from cache. Click Refresh for the latest data." with a Refresh button. Clicking Refresh bypasses KV cache (worker skips cache when
?refresh=1), fetches fresh data, then disables the Refresh button for the configured TTL so it cannot be overused. Banner text switches to "Data is up to date. Refresh available in X minutes." during cooldown. Backend:src/tracking/analyticsCache.tsexcludesrefreshfrom cache key;src/tracking/routes/analytics.tsskips cache lookup whenrefresh=1. Dashboard API:apiFetchWithCacheInfoand all cached analytics/moderation/lead-stats methods accept optionalCacheRefreshOptions(refresh?: boolean). HookuseCachedApinow takes a fetcher(opts?: { refresh?: boolean }) => Promise<...>and exposesrefetchWithRefresh()for the Refresh button. Cooldown is enforced client-side using the TTL returned inX-Cache-TTL-Seconds.
Changed
- Cache key no longer includes
refreshquery param so on-demand refresh and normal requests share the same key; live response after refresh is written back to cache.
2026-03-12 (Block/Unblock IP from listing pages)
Added
- Block/Unblock IP from listing pages — Team can block or unblock IPs from the table via a three-dots (kebab) Actions menu on each row. Leads (
/leads): Actions column with ⋮ opening a dropdown titled "Actions" with "Block IP" or "Unblock IP" per row. Testing leads (/testing-leads): "Block IP" in the row actions menu. Blocked IP leads (/blocked-ip-leads): "Unblock IP" in the row actions menu. Lead analytics – Lead submissions (/lead-analytics): "Block IP" or "Unblock IP" in the row actions menu. Reusable RowActionsMenu component (dashboard/src/components/row-actions-menu.tsx) so more actions can be added later without changing the layout. All actions use existingleads.blockIpandblockedIps.unblockAPIs; list refetches after success.
2026-03-12 (Sales CRM UX guide)
Added
- Sales CRM UX guide —
docs/ux/sales-crm-ux.md: comprehensive UX document for building a Sales CRM using the current DB schema while maintaining consistency with the existing dashboard. Covers data tables, side panel, filters (saved views, quick range, search, advanced bar), buttons, modals, badges, search, theme, and page patterns (list + side panel, full detail, kanban). Includes component inventory to reuse (Card, Button, Pagination, QuickRangeFilter, BulkActionsBar, ConfirmDestructiveModal, DropdownOptionSelect, etc.) and a CRM-specific checklist. VitePress sidebar: new "UX & Design" section with "Sales CRM UX guide".
2026-03-09 (Rate limiting: D1 atomic + whitelist bypass)
Changed
- Rate limiting — Counters moved from KV to D1 (
RateLimitCountstable, migration 039). Limits are now enforced under concurrency (no get-then-put race). Whitelisted IPs skip rate limiting on/track/*as documented. - docs/api/index.md — Rate limiting section updated (D1-backed, whitelist bypass).
Added
- Migration 039 —
RateLimitCountstable for atomic fixed-window rate limit counts.
2026-03-09 (Analytics response cache)
Added
- Analytics response cache — GET responses for lead-stats and moderation endpoints are cached in KV with a configurable TTL to reduce load on Analytics Engine and D1. Set
ANALYTICS_CACHE_TTL_SECONDS(e.g.300for 5 minutes);0or unset disables caching. Cached endpoints:engine/lead-stats,engine/lead-stats/summary,engine/projects,engine/sources, and allmoderation/*GET endpoints (traffic-quality, dashboard-summary, high-bot-by-source, aggregations, etc.). Cache key is endpoint + normalized query string. Responses served from cache include headerX-Cache: HIT. Implementation:src/tracking/analyticsCache.ts, wired insrc/tracking/routes/analytics.ts.
2026-03-09 (AI Analytics Assistant — Ask)
Added
- POST /analytics/ask — Natural language questions about traffic/lead quality. Request:
{ "question": "..." }. Response:answer,insights,recommended_actions,possible_blocks, optionalquery_plan. Auth: same as other analytics endpoints. - Query planner — Worker AI converts question → structured QueryPlan (analysis_type, filters). Only allowed types; no SQL.
src/ai/ask/types.ts,plannerPrompt.ts,queryPlanner.ts. - Query executor —
executeQueryPlan(env, plan)dispatches to predefined moderation/aggregation functions only.src/analytics/ask/queryExecutor.ts. - Result summarizer — Worker AI turns aggregated result + question → AskResponse.
src/ai/ask/summarizerPrompt.ts,resultSummarizer.ts. - Orchestration —
handleAsk(body, env)insrc/worker/askAnalyticsAssistant.ts. Question length capped at 500 chars. - docs/analytics/ai-analytics-assistant.md — API, allowed question types, safety, example requests. ARCHITECTURE.md — ai/ask, analytics/ask, worker/askAnalyticsAssistant.
2026-03-09 (AI moderation report)
Added
- AI moderation report —
GET /analytics/moderation/ai-reportreturns an aggregated moderation summary (last 24h or 7d) plus an AI-generated report (executive summary, issues, campaign problems, visitor abuse, recommended actions, blocking recommendations). Uses Cloudflare Worker AI (@cf/meta/llama-3-8b-instruct). Only aggregated data is sent to the AI; no raw submission rows. Query params:period=last_24_hours|last_7_days, optionalproject,source,skip_ai=true. - Moderation summary builder —
src/analytics/moderationQueries.ts:getModerationSummary(env, period)composes existing leadModerationQueries for totals, suspicious sources/campaigns/referers/countries, and high-risk visitors. - Worker AI binding —
[ai] binding = "AI"in wrangler.toml; optionalEnv.AIfor moderation report and future analytics assistant. - AI client and prompts —
src/ai/moderationPrompt.ts,src/ai/aiClient.tsfor structured JSON report generation. - Report orchestration —
src/worker/moderationReportWorker.ts:getModerationReport(env, options). - Traffic quality —
blocked_submissionscount (event_type = blocked) added togetTrafficQualityMetricsand to the moderation summary totals. - docs/analytics/ai-moderation-report.md — API and flow documentation. docs/analytics/traffic-moderation.md — New "AI moderation report" section. ARCHITECTURE.md — Added analytics/, ai/, worker/ layout.
2026-03-09 (Disable deprecated lead routes)
Changed
- POST /lead and POST / disabled — These legacy lead capture routes now return 410 Gone with a JSON body directing clients to use POST /lead.capture. No lead is created. Response includes
X-Deprecated: Use POST /lead.capture instead. GET /lead and GET / return 404.
2026-03-09 (Traffic moderation and quality)
Added
- Traffic moderation system — New Analytics Engine–backed queries and API endpoints for marketing to detect bad traffic and decide when to block sources, campaigns, or visitors. Dataset:
lead_submissions(3‑month retention). - Moderation UI (dashboard) — New Moderation page under Leads: Overview (traffic quality metrics + moderation summary cards), Suspicious traffic (high bot by source, suspicious referers, visitor abuse, campaign quality, country risk), Block candidates (rule-based table), Aggregations (by source, UTM campaign, referer, country). Filters: period, project, source. Available to marketing when the page is in their visible pages.
- Moderation queries — High bot rate by source/campaign, high suspicion by source, repeat submissions by visitor, suspicious referers, VPN-heavy by source, fast submits (time_to_submit < 3s), high duplicate rate by source.
- Visitor abuse detection —
GET /analytics/moderation/visitor-abusereturns visitors to flag by form_submit_count_24h, suspicion_score, vpn_score, or repeated bot submissions. - Campaign quality —
GET /analytics/moderation/campaign-qualityfor UTMs with high bot, low engagement, or high duplicate rate. - Country risk —
GET /analytics/moderation/country-riskfor countries with abnormal bot rate, high VPN, or low engagement. - Moderation dashboard —
GET /analytics/moderation/dashboard-summarywith counts of suspicious visitors, campaigns, referers, high-bot sources, and multi-form visitors. - Block candidates — Rule-based:
suspicion_score > 80 AND form_submit_count_24h > 5 AND vpn_score > 70→ BLOCK_CANDIDATE.GET /analytics/moderation/block-candidateswith optional rule overrides. - Traffic quality metrics —
GET /analytics/moderation/traffic-qualityfor bot rate, duplicate rate, avg suspicion/engagement, real lead rate. - Aggregations —
GET /analytics/moderation/aggregations/by-source,by-utm-campaign,by-referer,by-country,by-visitorfor sliced quality views. - docs/analytics/traffic-moderation.md — Design and usage guide for marketing users.
Changed
- docs/analytics/analytics-engine.md — Linked new traffic moderation section and doc.
2026-03-08 (Per-submission "View in CRM" link)
Added
- Per-submission View in CRM — Lead detail "All submissions" cards and the leads list drawer now show a "View in CRM" link on each submission that has a stored CRM lead ID (
LeadSubmissions.CrmLeadId). Same URL template as the lead-level link; opens in a new tab.
Changed
- GET /analytics/leads/:id — Submissions in the response now include
crm_lead_idandcrm_lead_url(fromLeadSubmissions.CrmLeadIdand the CRM URL template). - docs/changelog.md — This entry.
2026-03-08 (is_follow_up and CRM callback accepts both IDs)
Added
- Webhook payload:
is_follow_up— Make.com receivesis_follow_up: truewhen the submission is a follow-up (same person/project as an existing lead) andis_follow_up: falsefor the first submission. Lets Make.com branch without guessing.
Changed
- CRM callback: accept both lead and submission IDs —
POST /lead.capture/crm-callbacknow accepts bothlead_public_id/lead_idandlead_submission_public_id/lead_submission_idin the same request. We update bothLeads.CrmLeadIdandLeadSubmissions.CrmLeadIdso you don't need to know whether the webhook was primary or follow-up. Response includesupdated_leadandupdated_submissionbooleans. - docs/api/crm-callback.md, docs/api/lead-capture.md — Document
is_follow_upand recommended "send both IDs" callback usage.
2026-03-08 (Allow Make.com webhook in development)
Added
- Setting: Allow Make.com webhook in development — When the worker runs with
ENVIRONMENTother thanproduction, leads are not sent to Make.com by default. Admins can enable Allow sending to Make.com in development from Settings (shown only when environment is development). A confirmation modal and inline warnings remind that this triggers real webhook calls and should be turned off after testing. - Env fallback: ALLOW_MAKE_WEBHOOK_IN_DEVELOPMENT — Optional; when
trueor'true', leads are sent to Make.com in non-production. Defaultfalse. Overridden by the dashboard setting when set.
Changed
- Lead capture — Make.com send is allowed when
(devEnvironment === 'production' || appSettings.allow_make_webhook_in_development)and other conditions (send_leads_to_make, etc.) are met. - docs/features/settings.md — Documented the new setting.
2026-03-08 (Test lead by content)
Added
- Test lead by content — If name, email, project, or location contains any of a configured list of strings (case-insensitive), the lead is stored in TestingLeads and optionally sent to Make.com with
test_lead: yes, same as whitelist test leads. Configure the list in Settings → Lead flow & Make.com → Test lead by content (add/remove strings). Empty list = disabled. - Setting: test_lead_contains_strings — Stored in AppSettings (JSON array). Dashboard UI shows tags per string with remove; input + Add to add new. Send whitelist test leads to Make.com controls whether these content-based test leads are sent to Make.com.
- Env fallback: TEST_LEAD_CONTAINS_STRINGS — Optional comma-separated list in wrangler; used when not set in AppSettings.
Changed
- Lead capture — After IP whitelist check, if
test_lead_contains_stringsis non-empty and any of name/email/project/location contains any configured string, the request is routed to TestingLeads (and optional Make.com) instead of Leads/LeadSubmissions. - PATCH /analytics/settings — Accepts
test_lead_contains_strings(array of strings). - docs/features/settings.md, docs/features/ip-whitelisting.md, docs/features/environment-config.md — Test lead by content and TEST_LEAD_CONTAINS_STRINGS documented.
2026-03-08 (CRM callback secret from Settings)
Added
- CRM callback secret in Settings — Admins can set the CRM/Make.com callback secret from Settings → CRM & dashboard → CRM callback secret instead of (or in addition to) the
CRM_CALLBACK_SECRETenvironment variable. When set in Settings, it overrides the env. The value is stored in AppSettings (D1) and never returned to the dashboard; the UI shows "Configured" when a secret is set (from Settings or env) and offers "Clear stored" to remove the Settings value and fall back to env.
Changed
- CRM and Make callback handlers — Both use
getAppSettings(env).crm_callback_secret ?? env.CRM_CALLBACK_SECRETso Settings takes precedence when set. - GET /analytics/settings — Response includes
crm_callback_secret_configured(boolean) and never includes the raw secret. - docs/features/environment-config.md — CRM_CALLBACK_SECRET and Dashboard Settings sections updated.
2026-03-08 (Make.com callback: full JSON payload)
Added
- Make.com callback full payload (migration 038) — The make-callback endpoint now stores the entire request body in MakeWebhookSends.MakeCallbackPayload. You can send any extra JSON from Make.com (e.g. scenario name, duration_ms, steps_completed, custom fields); it is persisted and shown in the dashboard.
- Dashboard: Callback data from Make.com — In Sent to Make.com, when opening a row’s payload panel, a second section "Callback data from Make.com" shows the stored callback JSON (when present).
Changed
- POST /lead.capture/make-callback — Accepts any JSON body; still requires
make_send_public_idandmake_execution_url. Full body is stored. Backward compatible when migration 038 is not applied (callback still updates execution URL/status only).
2026-03-08 (Make.com execution callback & link)
Added
- Make.com execution callback — POST /lead.capture/make-callback: Make.com can send back the execution log URL (and optional execution ID/status) so we store it on MakeWebhookSends. Auth: same as CRM callback (
CRM_CALLBACK_SECRET). Body:make_send_public_id(from webhook payload),make_execution_url(required),make_execution_id,make_execution_status(optional). See Make callback API. - MakeWebhookSends execution fields (migration 037) —
PublicId(UUID sent asmake_send_public_idin webhook),MakeExecutionUrl,MakeExecutionId,MakeExecutionStatus. New sends includemake_send_public_idin the payload so Make.com can reference the row when calling the callback. - Dashboard: View in Make.com — Sent to Make.com list and payload panel show a "View in Make.com" link when an execution URL has been stored via the callback.
Changed
- docs/architecture.md — Route table includes
/lead.capture/make-callback. - docs/features/sent-to-make.md — Execution link behaviour and Make callback usage.
- docs/api/make-callback.md — New API reference for the Make.com execution callback.
2026-03-08 (Location & project dropdown options)
Added
- DropdownOptions table (migration 036) — New table
DropdownOptionswithKind(source | country | project | city),Value, optionalLabel,SortOrder. Used to store configured dropdown values for Source (e.g. Website, JustLead, Whatsapp), Country, Project, and City/Location. - Location & project config — Dashboard section Settings → Location & project (admin only): configure dropdown options per kind; add, edit, delete values. Options can be used in form configuration and lead filtering.
- Analytics API —
GET /analytics/dropdown-options(all grouped or?kind=source|country|project|city),POST /analytics/dropdown-options,GET/PATCH/DELETE /analytics/dropdown-options/:id. Admin only.
2026-03-08 (Lead stats chart UX)
Changed
- Lead stats page — Stacked area chart: one area per lead type (Primary, Follow-up, Test, Blocked, Bot) with legend and custom tooltip showing breakdown. New “Trend by lead type” section: five small area charts (one per type) with period total. Uses theme colors (
--color-chart-1–--color-chart-5), CartesianGrid, Legend. Improves visualization for campaign optimization.
2026-03-08 (Phase 11.3: Lead stats dashboard & export)
Added
- Lead stats page — Dashboard route
/lead-stats(sidebar: "Lead stats") with: days selector (7/14/30/90), time bucket (day / 60 min / 30 min), filters (project, source, country, city), breakdown by (none | type | project | source | country | city). Summary cards: total, primary, follow-up, test, blocked, bot, actual %. Volume-over-time chart (IST) when breakdown is none; bar chart when breakdown by type; table when breakdown by project/source/country/city. Top projects and top countries from summary API. - Export — Export JSON and Export CSV of current view (time series, grouped data, or summary) for AI or external tools.
- Marketing access —
/lead-statsadded to default marketing-visible pages.
Changed
- docs/planning/lead-stats-project-phases.md — Phase 3 tasks and status marked done.
2026-03-08 (Phase 11.2: Lead stats API)
Added
- Lead stats API —
GET /analytics/engine/lead-stats: time-bucketed counts by type (primary, follow_up, bot, test, blocked) from Analytics Engine only. Params:days(1–90),bucket(30min | 60min | day),project,source,country,city,group_by(none | type | project | source | country | city). Response: time series of buckets or grouped by dimension. Reporting timezone: IST (Asia/Kolkata) viatoStartOfInterval(..., 'Asia/Kolkata'). - Lead stats summary —
GET /analytics/engine/lead-stats/summary?days=7: last N days KPIs (total, by type, actual_percent, top_projects, top_countries) for AI or dashboard overview. - Lead stats service —
src/tracking/services/leadStatsService.ts:getLeadStats(),getLeadStatsSummary(), builds Analytics Engine SQL with filters and time buckets; no D1 queries.
Changed
- docs/analytics/analytics-engine.md — Lead stats API section: endpoints table, reporting timezone, query params, response shapes.
- docs/planning/lead-stats-project-phases.md — Phase 2 tasks and status marked done.
2026-03-08 (Phase 11.1: Analytics Engine schema & complete event stream)
Added
- Event type dimension (blob20) —
LeadSubmissionDataPointandwriteLeadSubmissionToAnalyticsEnginenow includeevent_type:primary|follow_up|test|blocked|bot. Normal path sets primary/follow_up; test leads, blocked-IP rejects, and honeypot (bot) leads each write one Analytics Engine data point with the corresponding event_type so lead-stats can be computed from Analytics Engine only (no D1). - City and region (blob21, blob22) —
userCityanduserRegionadded to the analytics point and written frompayload.user_city/payload.user_region(CF geo) for city-wise and region-wise reporting. - Test/blocked/bot writes — After
insertTestingLead, afterinsertBlockedIPLead(blocked IP), and after honeypot handling inhandleLead(bot, with or without BlockedIPLeads storage), the Worker callswriteLeadSubmissionToAnalyticsEnginewith the appropriate event_type and minimal dimensions.
Changed
- Schema doc —
docs/analytics/analytics-engine.md: blob20 (event_type), blob21 (user_city), blob22 (user_region); example queries for event_type and city; implementation details updated with all write call sites. - Phase tracking —
docs/planning/lead-stats-project-phases.md: Phase 1 tasks 1.1 and 1.2 marked done; Phase 1 status set to Done.
2026-03-07 (Lead stats project: future plan, phases, roadmap)
Added
- Future plan & phases —
docs/planning/lead-stats-project-phases.md: phased implementation plan for Lead Stats & Marketing Campaign Optimization (important project). Phase 1: Analytics Engine schema & event stream (event_type, city/region, test+blocked/bot writes). Phase 2: Lead stats API (time buckets, filters, groupings). Phase 3: Dashboard & reporting UI. Phase 4: AI-ready exposure and optional automation. Progress table for tracking. - Roadmap Phase 11 — Lead Stats & Marketing Campaign Optimization added to
ROADMAP.mdwith Tasks 11.1–11.4 aligned to the four phases. Next focus set to Phase 11 (start next); links to phased plan and planning doc.
Changed
- Planning doc —
docs/planning/lead-stats-marketing-campaigns.md: added implementation-tracking note at top with links to future plan and ROADMAP Phase 11.
2026-03-07 (Planning: lead stats for marketing campaigns)
Added
- Planning doc —
docs/planning/lead-stats-marketing-campaigns.md: whether we already store data for (1) time-bucketed lead volume (30/60 min or day-wise) and (2) breakdown by type (actual, follow-up, bot, test, VPN, suspicious, hot). Confirms all required data exists in Leads, LeadSubmissions, TestingLeads, BlockedIPLeads, and Analytics Engine; notes timezone and single-report gaps; recommends next steps (define time window, spec report API, optional view/denormalization). No implementation.
2026-03-07 (Lead drought alert)
Added
- Lead drought alert — Cron runs every 10 minutes. If no lead has been captured within a configurable threshold (default 30 minutes), an alert is sent to an optional Make.com webhook URL. Configurable from Settings: enable/disable, webhook URL, threshold minutes, cooldown minutes (default 120), and business hours (optional start/end/timezone so alerts are only sent during that window). Last-alert time stored in KV. See
docs/features/lead-drought-alert.mdanddocs/planning/lead-drought-alert.md. - Backend —
src/lib/leadDroughtAlert.ts:getLastLeadAt(env),runLeadDroughtCheck(env, log),isWithinBusinessHours(now, start, end, timezone); scheduled handler branches on cron (*/10 * * * *→ lead drought,0 3 * * *→ R2 retention). AppSettings:lead_drought_alert_enabled,lead_drought_alert_webhook_url,lead_drought_threshold_minutes,lead_drought_cooldown_minutes,lead_drought_business_hours_enabled,lead_drought_business_hours_start,lead_drought_business_hours_end,lead_drought_business_hours_timezone. PATCH /analytics/settings accepts the new fields. - Dashboard — Settings card Lead drought alert: toggle, Make.com webhook URL, threshold and cooldown; business hours (toggle, start/end 24h, IANA timezone); save with existing Save changes.
- Tests —
test/leadDroughtAlert.spec.js: isWithinBusinessHours (within/outside window, invalid input), getLastLeadAt (empty/recent), runLeadDroughtCheck (disabled / no leads / business hours enabled), scheduled handler for */10 cron.
2026-03-07 (Docs cleanup for production launch)
Changed
- Architecture —
docs/architecture.mdsynced with full source layout, request flow (all routes including CRM callback/webhook, forms, replay, pulse.js), bindings, D1 schema summary, and migrations (001–035+). RootARCHITECTURE.mdmigration section now points tomigrations/and apply scripts. - VitePress — Full sidebar: all API docs (CRM callback, CRM webhook, Forms config), all feature docs (Form builder, Lead scoring, Fraud detection, Sent to Make, Settings, IP blocking/whitelisting, Marketing access, Environment config, Session replay). Deployment added to nav and Overview.
- Deployment — Production launch checklist at top of
docs/deployment.md. D1 migrations section simplified to script-only (scripts/apply-production-migrations.sh, 001–035).CRM_WEBHOOK_SECRETadded to GitHub secrets list and Summary table. - API reference — Removed "Phase 8" from CRM Callback and CRM Webhook titles. API index documents role-based access (admin vs marketing) with link to Marketing access.
2026-03-07 (Consistent PII masking and lead detail cards)
Added
- Single masking utilities — Backend
src/lib/masking.ts:maskEmail(e.g.j***@***.com),maskPhone(e.g.***-***-1234),maskName(e.g.J***),maskPayloadJson(masks EMAIL/PHONE/FIRSTNAME inside JSON payloads),maskSubmissionRow(contact fields + payload_json). Used everywhere so marketing never sees raw PII. - Dashboard display helpers —
dashboard/src/lib/masking.ts: same rules as backend;displayEmail,displayPhone,displayNametake role and return masked when marketing, raw when admin. All lead-related pages use these for consistent display.
Changed
- Lead detail submissions — When marketing fetches lead detail,
payload_jsonin each submission is now masked (EMAIL/PHONE/FIRSTNAME replaced with the same format as contact fields). Lead detail cards and follow-up history no longer show unmasked email/phone from the payload. - Sent to Make.com — Replaced ad-hoc
***withmaskContactFields,maskName, andmaskPayloadJsonso sent-to-make list and payload viewer show the same mask format as leads. - Leads, Lead detail, Lead analytics, Sent to Make, Testing leads, Blocked IP leads — Dashboard uses
displayEmail/displayPhone/displayNamewhen rendering contact info so marketing never sees raw values even if the API response were wrong.
2026-03-07 (Marketing API keys: generate in Settings, per-key page settings)
Added
- Marketing API keys — Admins generate marketing API keys in Settings → Marketing API keys. Each key is auto-generated (e.g.
pg_mkt_…), shown once at creation, with regenerate and delete. Each key has its own visible pages: when creating or editing a key, admins choose which pages that key can access. Leads, Sent to Make.com, Settings, and Forms config remain admin-only. - Backend — Migration 035 adds MarketingApiKeys table (KeyHash, KeyPrefix, VisiblePages JSON). Auth: admin key from env; env
ANALYTICS_MARKETING_API_KEY(optional) uses global AppSettings visible pages; any other key is looked up by SHA-256 hash in MarketingApiKeys and gets that row's visible pages. GET/POST /analytics/marketing-keys, GET/PATCH/DELETE /analytics/marketing-keys/:id, POST /analytics/marketing-keys/:id/regenerate (admin-only). GET /analytics/settings/banner returnsmarketing_visible_pagesfrom the current key (per-key or global). - Dashboard — Settings card "Marketing API keys": list keys (name, prefix, visible pages summary), Generate new key (name + checkboxes), Edit, Regenerate (confirm; new key shown once), Delete (confirm). Copy-to-clipboard for newly created/regenerated keys.
Changed
- Auth —
authenticateAnalyticsis async and acceptsenv; resolves marketing role from env key (global pages) or DB key (per-key pages). handleAnalytics receives full auth result and usesmarketingVisiblePagesfor the banner. - Docs — settings.md, marketing-access.md updated.
2026-03-07 (Marketing API visible pages)
Added
- Marketing API visible pages — New app setting so admins can control which pages marketing API users can navigate to. Settings → “Marketing API visible pages” card lists non-admin pages (Dashboard, Lead analytics, Testing leads, Blocked IP leads, Blocked IPs, Whitelisted IPs, Sessions, Replay); admins check/uncheck to allow or deny access. Unchecked pages are hidden in the sidebar for marketing and redirect to the dashboard if opened directly.
- Backend —
marketing_api_visible_pagesstored in AppSettings (JSON array of path strings). GET /analytics/settings/banner now returnsmarketing_visible_pagesso the dashboard can filter nav and enforce route access. PATCH /analytics/settings acceptsmarketing_api_visible_pages. - Dashboard — MarketingPageGuard redirects marketing users from disallowed paths; sidebar shows only allowed pages for marketing.
Changed
- Docs — settings.md and marketing-access.md updated.
2026-03-07 (Sent to Make.com dashboard section)
Added
- Sent to Make.com — New dashboard section that lists payloads successfully sent to the Make.com webhook. Table shows sent time, name, email, phone, project, lead link, and expandable/copyable payload JSON.
- Filters — Quick range (Today, Yesterday, Past 7/14/30 days, All time, Date range), search by email/phone/name, and optional
?lead_id=filter. Pagination with configurable page size. - Navigation — Lead detail page has a "View in Sent to Make.com" button that opens the Sent to Make page filtered by that lead. Sent to Make table has a Lead column linking to lead detail.
- Backend — Migration 033 adds MakeWebhookSends table. On successful send (normal and testing leads), the payload is logged to this table. GET /analytics/leads/sent-to-make returns the list with filters (admin only).
Changed
- Lead capture — After
sendWebhookWithRetry(WEBHOOKS.MAKE, …)succeeds,insertMakeWebhookSend(env, payload)is called so the dashboard can display the payload.
2026-03-07 (Setting: mask email and phone for marketing)
Added
- Mask email and phone for marketing — New app setting (dashboard + AppSettings table + env fallback
MASK_EMAIL_PHONE_FOR_MARKETING). When on (default), users with the marketing API key see masked email and phone in Testing leads, Blocked IP leads, and Lead analytics submissions. When off, marketing sees full contact info. - Dashboard: Toggle "Mask email and phone for marketing" on Settings → Sending leads to Make.com card; Save changes persists the value.
Changed
- Analytics route — PII masking for marketing is applied only when
settings.mask_email_phone_for_marketingis true. Settings are loaded once per request and used for blocked-ip-leads, testing-leads, and leads/submissions responses. - Docs — settings.md and environment-config.md updated.
2026-03-07 (Control lead submission charts visibility via Settings)
Added
- Show lead submission charts — New app setting (dashboard + AppSettings table + env fallback
SHOW_LEAD_SUBMISSION_CHARTS). When off, the Lead submission analytics page does not query Analytics Engine and shows a message that charts are hidden; the submissions table is still shown. When on (default), charts load as before. - Dashboard: Toggle "Show lead submission charts" on Settings → Sending leads to Make.com card; Save changes persists the value.
- GET /analytics/settings/banner — Response now includes
show_lead_submission_chartsso the lead analytics page can hide charts without calling admin-only settings.
Changed
- Lead analytics page — Fetches
settings.banner(); whenshow_lead_submission_chartsis false, skips all Analytics Engine API calls and shows a card plus the submissions table only. - Docs — settings.md and environment-config.md updated.
2026-03-07 (CRM lead URL template in Settings)
Added
- CRM lead URL template — New app setting (dashboard + AppSettings table + env fallback
CRM_LEAD_URL_TEMPLATE). Admins can set the "View in CRM" URL template from Settings (e.g.https://crm.example.com/lead/). When set, lead detail uses it to build the link; when empty, the link is hidden. - Dashboard: "CRM lead URL template" text input on Settings → Sending leads to Make.com card; Save changes persists the value.
Changed
- Lead detail — "View in CRM" URL is now resolved from
getAppSettings(env).crm_lead_url_templatewith fallback toenv.CRM_LEAD_URL_TEMPLATE. - Docs — settings.md and environment-config.md updated.
2026-03-07 (Option to stop storing honeypot leads in D1)
Added
- Store honeypot leads in D1 — New app setting (dashboard + AppSettings table + env fallback
STORE_HONEYPOT_LEADS). When off, honeypot (bot) leads are no longer written to BlockedIPLeads; the IP is still blocked. When on (default), behaviour is unchanged (honeypot leads stored for audit). - Dashboard: Checkbox "Store honeypot leads in D1" on Settings → Sending leads to Make.com card; Save changes persists the value.
Changed
- Lead flow (honeypot path) — After blocking the IP,
insertBlockedIPLeadis called only whenappSettings.store_honeypot_leadsis true. Log messages distinguish "IP blocked (not stored in D1)" vs "stored in BlockedIPLeads, IP blocked". - Docs — settings.md, environment-config.md updated; STORE_HONEYPOT_LEADS added to wrangler.toml.
2026-03-07 (Banner when leads are not sent to Make.com)
Added
- Dashboard banner — When "Send leads to Make.com" is off, a persistent amber banner appears at the top of every dashboard page (for all roles) stating that leads are not being sent to the CRM. Admins see an "Enable in Settings" link to
/settings. - GET /analytics/settings/banner — Lightweight endpoint returning
{ send_leads_to_make: boolean }. Allowed for both admin and marketing so the banner can be shown without exposing full settings.
2026-03-07 (Settings in database: admin can control Make.com toggles anytime)
Added
- AppSettings table (migration 032) — Key-value store in D1 for
send_leads_to_make,send_test_leads_to_make,bot_lead_detection,lead_reengage_hours. Admins can change these from the dashboard without redeploying. - PATCH /analytics/settings (admin-only) — Accepts partial
{ send_leads_to_make?, send_test_leads_to_make?, bot_lead_detection?, lead_reengage_hours? }, persists to AppSettings, returns updated full settings. - Dashboard: editable Make.com settings — Settings page "Sending leads to Make.com" card now has checkboxes and a number input; Save changes persists to the database. Changes take effect immediately for new lead submissions.
Changed
- Lead flow — Worker reads Make.com-related flags from AppSettings (D1) with fallback to wrangler env when a key is missing or table does not exist. One
getAppSettings(env)call per lead request. - GET /analytics/settings — Now returns values from AppSettings (with env fallback) instead of env only.
- Docs — settings.md and environment-config updated to describe database-backed settings and PATCH API.
2026-03-07 (Setting: send whitelist test leads to Make.com)
Added
- SEND_TEST_LEADS_TO_MAKE env var — When
falseor'false', leads from IPs whitelisted with usage "test" are not sent to Make.com (still stored in TestingLeads). Defaulttrue. Set in wrangler[vars]or per-env. - Settings UI — "Send whitelist test leads to Make.com" row on the Sending leads to Make.com card; value comes from
GET /analytics/settings(send_test_leads_to_make). - Docs — environment-config, settings, and ip-whitelisting updated to describe the toggle.
Changed
- Lead capture (testing leads) — Send-to-Make for whitelist test leads now also checks
parseSendTestLeadsToMake(env). - Wrangler —
SEND_TEST_LEADS_TO_MAKE = trueadded to default[vars],env.development.vars, andenv.production.vars.
2026-03-07 (Control over sending leads to Make.com)
Added
- SEND_LEADS_TO_MAKE env var — When
falseor'false', leads are never sent to Make.com (still stored in D1). Defaulttrue. Set in wrangler[vars]or per-env; applies to both normal and testing leads. - GET /analytics/settings (admin-only) — Returns
environment,send_leads_to_make,bot_lead_detection,lead_reengage_hoursfor the dashboard. Marketing receives 403. - Settings page: Sending leads to Make.com — New card on Settings that fetches and displays current state (On/Off for send and bot detection, re-engage hours, environment). Includes note on changing via wrangler vars and redeploy.
Changed
- Lead capture — Send-to-Make decision now also checks
parseSendLeadsToMake(env); testing leads path respects it as well. - Wrangler —
SEND_LEADS_TO_MAKE = trueadded to default[vars],env.development.vars, andenv.production.vars.
2026-03-07 (Settings section: admin-only configuration hub)
Added
- Settings section — Dedicated Settings area in the dashboard (sidebar group "Settings") with a hub page at
/settingslinking to Forms config and a placeholder for future environment-level controls. Entire section is admin-only: sidebar group and routes are hidden for marketing;/settingsand/forms-configare wrapped inAdminRouteand redirect marketing users to the dashboard. - Settings page (
/settings) — Landing page with cards: "Forms config" (link to/forms-config) and "Environment & controls" (placeholder for future options such as bot detection, retention, feature flags).
Changed
- Forms config — Moved under Settings in the sidebar; now admin-only (previously visible to marketing). URL
/forms-configunchanged. - Sidebar — "Config" group replaced by "Settings" group (admin-only) with items "Settings" and "Forms config".
2026-03-07 (Forms config: dashboard UX)
Added
- Table and Tabs UI components — Dashboard adds Shadcn-style
Table(TableHeader, TableBody, TableRow, TableHead, TableCell) and controlledTabs(TabsList, TabsTrigger, TabsContent) for consistent listing layout. - Forms config page UX — Forms config listing reworked for clarity and alignment with dashboard design: Sites and Integrations are in separate tabs; Sites use a table with expandable rows (name, site key, status, actions); Integrations use a table (name, masked API key, Revoke). Setup guide is in a collapsible section so the main content is the listing.
- Confirm modals for destructive actions — Remove domain, delete form, and revoke API key now use
ConfirmDestructiveModalinstead ofconfirm(), with loading state and clear copy.
Changed
- Forms config page layout — Page title has a short description; Sites and Integrations each in a card with table layout; row actions (expand, domain remove, form edit/embed/delete, revoke) unchanged in behaviour, improved presentation.
2026-03-07 (Soft delete: Testing leads & Blocked IP leads)
Added
- Soft delete for Testing leads and Blocked IP leads — Migration 031 adds
DeletedAt(TEXT, NULL) toTestingLeadsandBlockedIPLeads. "Remove selected" in the dashboard no longer hard-deletes: it setsDeletedAtto the current timestamp. Rows withDeletedAtset are excluded from list API and dashboard so the listing stays clean, but data is retained in the DB for later analysis.
Changed
- Bulk remove behaviour —
POST /analytics/testing-leads/bulk-deleteandPOST /analytics/blocked-ip-leads/bulk-deletenow perform a soft delete (UPDATE ... SET DeletedAt = ?) instead of DELETE. List endpoints filter withDeletedAt IS NULLso removed rows are not shown.
2026-03-07 (Testing leads & Blocked IP leads: phone with country code)
Added
- ContactCountryCode on BlockedIPLeads and TestingLeads (migration 030) — Both tables now have
ContactCountryCode(formCOUNTRYCODEat capture). New submissions store country code; list API returnscontact_country_codeso the dashboard can show phone with country code (e.g. +91-9876543210).
Changed
- Dashboard: Testing leads & Blocked IP leads — Phone column shows formatted phone with country code (same as Leads list) and click-to-call link when phone is present. Backend insert/list support both pre- and post-migration 030 (fallback when column is missing).
2026-03-07 (Marketing team access: role-based API keys and PII masking)
Added
- Role-based analytics auth — Worker accepts two API keys:
ANALYTICS_API_KEY(admin) and optionalANALYTICS_MARKETING_API_KEY(marketing). Auth returns role so analytics routes can enforce permissions and masking. - PII masking (
src/lib/masking.ts) —maskEmail,maskPhone,maskContactFieldsfor marketing responses. Email:j***@***.com; phone:***-***-1234. - Marketing restrictions — For marketing role:
GET /analytics/leads(list) andGET /analytics/leads/:id(detail) andGET /analytics/leads/recent-submissionsreturn 403 Forbidden. All other analytics endpoints allowed; lead/submission payloads (testing-leads, blocked-ip-leads, leads/submissions) return masked email/phone. - Overview role —
GET /analytics/overviewresponse includesrole(admin|marketing) so the dashboard can adapt UI. - Dashboard role support — Connect page stores
rolefrom overview; sidebar hides “Leads” for marketing;AdminRouteredirects marketing from/leadsand/leads/:idto dashboard. Lead analytics remains visible with masked submissions.
Changed
- Auth —
authenticateAnalytics(request, adminKey, marketingKey, log)returns either{ response }or{ role }.authenticateApiKeyretained for compatibility (single key).
2026-03-07 (Bulk operations: shared components for listing pages)
Added
- useBulkSelection hook (
dashboard/src/hooks/use-bulk-selection.ts) — Manages multi-select state:selectedIds,selectedCount,isSelected(id),toggle(id),selectAll(ids),clearSelection,setSelection,isAllSelected(ids),isSomeSelected(ids). Row ids can be string (e.g. IP) or number (e.g. record id). - BulkActionsBar (
dashboard/src/components/ui/bulk-actions-bar.tsx) — Bar shown when at least one row is selected: “N items selected”, “Clear selection” button, and slot for action buttons (e.g. “Unblock selected”). Hidden when nothing selected. - Checkbox (
dashboard/src/components/ui/checkbox.tsx) — Styled checkbox with optionalindeterminatefor “select all” (some selected). Used for row selection and header “select all on page”. - Blocked IPs page — First listing to use bulk operations: checkbox column, select-all header, bulk “Unblock selected” action. Other listing pages (Whitelisted IPs, Blocked IP leads, Testing leads, etc.) can reuse the same pattern.
2026-03-07 (Testing leads: separate table + test_lead in Make.com payload)
Added
- TestingLeads table (migration 029) — When a submission is received at
/lead.capturefrom an IP whitelisted with usage "test" (testing leads), the lead is not inserted into Leads/LeadSubmissions. It is stored in TestingLeads (same shape as BlockedIPLeads) and sent to Make.com with test_lead: yes in the payload for clear separation. Client gets the same success response. - Lead capture flow — After the blocked-IP check,
getWhitelistedIpUsage(env, requestIP)is used; if'test', we insert into TestingLeads, setpayload.test_lead = 'yes', send webhook to Make.com, and return (no handleLead). - GET /analytics/testing-leads — List testing leads with
limit,offset,ip,period(same quick ranges as blocked-ip-leads). Returnstesting_leadsandtotal. - Dashboard: Testing leads — New page at
/testing-leadswith table of submissions from whitelisted test IPs (received at, IP, contact, email, phone, project, source, referer), filter by IP and quick range. Sidebar link "Testing leads" with FlaskConical icon.
2026-03-07 (Whitelisted IPs: Usage column — test vs internal)
Added
- WhitelistedIPs.Usage (migration 028) — Column indicating whether the IP is for testing lead sending (
test) or actual lead sending from internal system (internal). Defaultinternal. Dashboard add form includes a Usage dropdown; table shows a Usage column (Testing leads / Internal actual). API list returnsusage; POST accepts optionalusage("test"|"internal").
2026-03-07 (IP whitelisting: dashboard and API)
Added
- Whitelisted IPs — New WhitelistedIPs table (migration 027) and dashboard section at
/whitelisted-ips. Admins can add/remove IPs and optionally set a note (e.g. "Internal office", "CI server"). Requests from whitelisted IPs can skip certain checks; useisIpWhitelisted(env, ip)in the worker where needed. - API —
GET /analytics/whitelisted-ips(list withip,limit,offset),POST /analytics/whitelisted-ips(body:ip, optionalnote,usage),DELETE /analytics/whitelisted-ips/:ip(remove from whitelist). - Dashboard — Whitelisted IPs page with add form (IP, Usage, optional note), filter by IP, paginated table with Usage column, and remove action. Sidebar link "Whitelisted IPs" with ShieldCheck icon.
2026-03-06 (Blocked IPs pagination; shared Pagination component)
Added
- Blocked IPs list pagination —
GET /analytics/blocked-ipssupportslimitandoffset; response includestotal. Dashboard Blocked IPs page shows 25 per page with Previous/Next. - Shared Pagination component —
dashboard/src/components/ui/pagination.tsx: reusable bar with “Page X of Y (total)” and Previous/Next buttons. Used on Blocked IPs and Blocked IP leads for a consistent experience.
2026-03-06 (Blocked IP leads: date range filter)
Added
- Blocked IP leads: Quick range filter — The Blocked IP leads list (API and dashboard) supports a date filter via
period: Today, Yesterday, Past 7 days, Past 2 weeks, Past 30 days, All time. Dashboard shows a “Quick range” row with calendar icon and preset buttons; API accepts?period=today|yesterday|past_7_days|past_14_days|past_30_days|all.
2026-03-06 (Reject blocked IP at capture; Blocked IP leads table and dashboard)
Added
- Blocked IP check at /lead.capture — Before inserting into Leads/LeadSubmissions or sending to Make.com, the request IP is checked against the BlockedIPs table. If the IP is blocked, the submission is rejected: not stored in Leads or LeadSubmissions, not sent to Make.com. The client receives the same success/redirect response so the form does not reveal that the IP is blocked.
- BlockedIPLeads table (migration 026) — Rejected submissions from blocked IPs are recorded in
BlockedIPLeads(Ip, ReceivedAt, ContactEmail, ContactName, ContactPhone, ProjectName, Source, Referer, PayloadJson, SiteKey, FormId) for audit and dashboard visibility. - GET /analytics/blocked-ip-leads — Returns paginated list of rejected leads (
blocked_ip_leads,total). Query params:limit,offset,ip(filter by IP). - Dashboard: Blocked IP leads — New section at
/blocked-ip-leadswith a table of rejected submissions (received at, IP, contact, email, phone, project, source, referer) and filter by IP.
Changed
- lead.ts — After auth/domain checks, calls
isIpBlocked(env, userIPOriginal). If true, callsinsertBlockedIPLead(env, …)and returnsbuildRedirectResponse(...)without callinghandleLead, so no DB insert (Leads/LeadSubmissions) and no Make.com webhook.
2026-03-06 (Primary lead card blocked status)
Fixed
- Primary lead card not showing blocked status — When an IP is in the block list but the main lead-detail query (e.g. join with
BlockedIPs) did not setis_ip_blocked, the primary lead card could still show "Block IP" and no red styling. The backend now performs a fallback lookup: if the lead has a non-emptycontact_ipandis_ip_blockedis 0, it runsSELECT 1 FROM BlockedIPs WHERE Ip = ?and setsis_ip_blocked: 1when the IP is found. The primary card then correctly shows red border/background, "IP blocked" badge, and "Blocked" next to the IP.
2026-03-06 (Lead submissions listing: IP and blocked status)
Added
- IP column and blocked status on Lead submissions table — The “Lead submissions” table (Lead submission analytics → Lead submissions section) now has an IP column showing each submission’s
contact_ip. Rows whose IP is blocked use red left border and light red background; the Type column shows a “Blocked” pill next to Primary/Follow-up when that submission’s IP is blocked.
Changed
- Backend —
GET /analytics/leads/submissions(used by the Lead submissions table) now returnscontact_ipandis_ip_blockedper row via a join withBlockedIPs. - Dashboard —
LeadSubmissionListItemtype and Lead submissions table inlead-analytics.tsxupdated to display IP and blocked state.
2026-03-06 (Lead detail and submissions: blocked state updates)
Added
- Per-submission
is_ip_blocked—GET /analytics/leads/:idsubmissions now includeis_ip_blocked(1 when that submission’s IP is in BlockedIPs). Lead detail and side-panel submission lists show “IP blocked” styling and badge per submission.
Changed
- Lead detail after Block IP — After blocking an IP from the lead summary or a submission card, the page updates in place (no full reload): summary card and submission cards immediately show blocked styling and “IP blocked” / “Blocked” via a silent refetch.
- Submission cards (lead detail) — Each submission card uses red border/background and “IP blocked” badge when that submission’s IP is blocked; “Block IP” is hidden and “Blocked” badge shown when already blocked.
- Side panel submission list — Each submission row shows “IP blocked” badge and red left border when that submission’s IP is blocked; after blocking from the panel, the list and lead summary update without a loading flash using silent refetch.
- useApi — New
refetchSilent()that refetches and updates data without setting loading, so blocking actions can refresh lead/submission data without a loading spinner.
Fixed
- Lead detail not updating after Block IP — Blocked state now appears immediately after blocking (summary card and submission cards) by refetching lead detail silently and rendering per-submission
is_ip_blockedfrom the API.
2026-03-06 (Block IP for leads)
Added
- IP in leads and Block IP — Lead list and detail now show the lead’s IP (
contact_ip). Admins can block an IP from the lead detail page or the leads side panel; blocked IPs are stored in a newBlockedIPstable. Leads whose IP is blocked are shown with distinct styling (red tint, “Blocked” badge) in the table, kanban cards, side panel, and full lead detail page. - Migration 024 —
BlockedIPstable (Ip, BlockedAt, Reason). Apply withscripts/apply-production-migrations.shor runmigrations/024_blocked_ips.sqlmanually. - POST /analytics/blocked-ips — Body
{ "ip": "1.2.3.4", "reason": "optional" }adds the IP to the block list (re-adding updates BlockedAt and Reason). Requires analytics API key. - GET /analytics/blocked-ips — Returns
{ blocked_ips: [{ ip, blocked_at, reason, country, referer }] }. Optional query?ip=or?q=filters by IP (substring). - DELETE /analytics/blocked-ips/:ip — Removes the IP from the block list (IP is URL-encoded in path).
- Blocked IPs dashboard page — New section at
/blocked-ips: list all blocked IPs with reason, country, referer, and blocked-at; filter by IP; add new blocked IP (with optional reason, country, referer); unblock from the table. - Honeypot auto-block — When a honeypot (bot) lead is detected, the lead’s IP is blocked immediately with reason "Honeypot"; source country (Cloudflare
cf.country) and request referer are stored in BlockedIPs. - Migration 025 —
BlockedIPstable: addCountryandReferercolumns. Run after 024. - List/detail API —
GET /analytics/leadsandGET /analytics/leads/:idnow returncontact_ipandis_ip_blocked(1 when the lead’s IP is in BlockedIPs).
Changed
- Dashboard leads page — New IP column in table view; IP and “Block IP” in side panel; table rows and kanban cards use red styling when
is_ip_blocked; side panel shows red border when lead’s IP is blocked. - Dashboard lead detail page — IP address with “Block IP” button; summary card uses red border/background when IP is blocked; “IP blocked” badge when blocked.
2026-03-06 (Lead submission requires auth)
Changed
- POST /lead.capture and POST /lead — Submissions are rejected with 403 unless one of: (1) X-API-Key header (valid integration API key for server-to-server), or (2) siteKey + formId in the request body (browser-embedded form; domain and site/form enabled are then validated). Requests with no X-API-Key and missing or empty siteKey or formId now receive
403and a clear error message instead of being processed.
2026-03-06 (Form requires site key and form ID)
Changed
- Form rendering — Site key and form ID are now required. If
PulseGate.init()is called withoutsiteKey, orPulseGateForm.render()is called withoutformId, the form does not render; target containers show "Form unavailable. Site key and form ID are required." (or "Form ID is required") and the script returns early. This removes the previous "simple embed" mode so all forms are loaded from the backend and subject to domain and enabled checks. - docs/features/form-builder.md — Updated to describe required
siteKeyandformId; Quick Start and Minimal integration examples now usePulseGate.init({ siteKey })andrender({ formId, ... }).
2026-03-06 (Form builder docs)
Changed
- docs/features/form-builder.md — Updated for current form implementation: added intro and path A/B (simple embed vs multi-form/sites), script URL table, prerequisites and backend form JSON for multi-form, optional target from backend, troubleshooting table (403, Form unavailable),
formId/siteKey/targetin render() API, and Quick reference section at the end.
2026-03-06 (Disabled site/form: no cache + reject submissions)
Fixed
- Disabled site/form still showing and accepting submissions — When a site or form is disabled,
/forms/configreturns 403 but the form could still appear and accept submissions because: (1) responses were cacheable, so browsers/CDN could serve a previously cached 200 and form schema; (2)/lead.capturedid not check site/form enabled and accepted submissions anyway. Changes: (1) All/forms/configresponses now sendCache-Control: no-store, no-cache, must-revalidateso config is not cached. (2) Form script fetches config withfetch(..., { cache: 'no-store' })so the client does not use cached config. (3)/lead.capturenow callscheckSiteAndFormEnabled()after domain validation and returns 403 with "Site is disabled" or "Form is disabled" when the site or form is disabled, so submissions are rejected even if the form was loaded from cache.
Changed
- src/forms.ts — Added
NO_STORE_HEADERSand use them on all/forms/configresponses; added exportedcheckSiteAndFormEnabled(env, siteKey, formId?)for use by lead capture. - src/lead.ts — After domain validation for browser submissions with
siteKey, checks site and form enabled viacheckSiteAndFormEnabledand returns 403 if disabled. - src/formBuilder/formScript.ts — Config fetch calls use
{ cache: 'no-store' }.
2026-03-06 (Local D1 migrations script fix)
Changed
- Local migrations script —
scripts/apply-local-migrations.shnow targets the local D1 database using--localinstead of--remoteand no longer relies on an unsetENVvariable, so running it fully provisions the dev SQLite database forwrangler dev. - Migration 015 — Updated
015_leads_referer_column.sqlto add aReferercolumn toLeadsinstead of renamingProjectWebsite, making the migration idempotent and safe for freshly created local databases while keeping production schemas unchanged.
2026-03-06 (Multi-form / sites architecture)
Summary
Multi-form support with sites, authorized domains, form schema in the database, and server-to-server lead capture via X-API-Key. Existing SDK usage remains supported.
Added
- Sites, SiteDomains, Forms, Integrations — New tables (migration 021):
Sites(SiteKey),SiteDomains(allowed domains per site),Forms(form schema JSON per form ID),Integrations(API keys for server-to-server). - Leads/LeadSubmissions SiteKey and FormId — Migration 022 adds
SiteKeyandFormIdtoLeadsandLeadSubmissionsfor attribution. - GET /forms/config — Returns form schema by
siteKeyandformId; validates request domain againstSiteDomains; returns 403 if domain not allowed. - Domain validation —
validateDomainForSite()uses Origin/Referer hostname andSiteDomains; used by/forms/configand by/lead.capturein browser mode whensiteKeyis present. - API key validation —
validateIntegrationApiKey()looks up X-API-Key inIntegrations; used by/lead.capturein server mode. - Lead capture two modes — (1) If
X-API-Keyheader present: validate against Integrations, skip domain check, store lead. (2) If not: when body hassiteKey, validate domain; then store lead with optionalsiteKey/formId. - Form builder — When
PulseGateForm.render({ formId, target, ... })is called andsiteKeyis set (e.g. viaPulseGate.init({ siteKey: 'pk_live_xxx' })), the script fetches/forms/config, merges backend form schema with configure/render options, and renders fields dynamically. Submissions includesiteKeyandformIdin the payload. - PulseGate.init({ siteKey }) — Combined script now stores
PulseGate.siteKeyfor use by the form when fetching config.
Changed
- Lead handler — Runs domain or API-key validation before processing; writes
SiteKeyandFormIdto Leads and LeadSubmissions when provided in the body. - Form script — Async path when formId + siteKey present: loading placeholder → fetch config → merge and render; sync path unchanged when no formId/siteKey.
Files
migrations/021_sites_forms_integrations.sql,migrations/022_leads_site_form_columns.sqlsrc/forms.ts— handleFormsConfig, validateDomainForSite, validateIntegrationApiKeysrc/lead.ts— LeadRequestBody siteKey/formId, validation branch, writeToD1/writeLeadSubmission with SiteKey/FormIdsrc/index.ts— GET /forms/config route, PULSE_INIT siteKeysrc/formBuilder/formScript.ts— render() async fetch config, merge schema, hidden siteKey/formIddocs/api/forms-config.md,docs/api/lead-capture.md,docs/api/index.md,ARCHITECTURE.md
2026-03-06 (Lead event stack: single log per request)
Summary
Lead capture requests now emit one consolidated log line per request with an event_stack array, so Cloudflare (and other log consumers) show a single log entry containing the full sequence of lead events instead of many separate lines.
Added
- Lead event stack —
logLeadEventaccepts an optionaleventStack; when provided, events are pushed to the stack and no individualconsole.log/console.warn/console.erroris emitted. At the end of the request,flushLeadEventStack()logs one JSON line:{ lead_request: true, event_stack: [...], event_count: N, ts }. Log level of the single line is derived from the stack (error if any error, else warn if any warn, else info). - Exported types —
LeadEventEntryandLeadEventStackfor use by tests or tooling.
Changed
- Main lead flow —
handleRequestcreates aleadEventStack, passes it throughhandleLead,buildRedirectResponse,lookupVisitorScore,findExistingLeadForProject,isDuplicateLead,writeToD1,writeLeadSubmission, andbuildRedirectURL. AlllogLeadEventcalls in that path use the stack; the stack is flushed once before returning the response (and on early return or catch). - CRM callback / webhook — Unchanged; they do not use a stack and continue to log each event as a separate line (typically one or two per request).
Files
src/lead.ts—LeadEventEntry,LeadEventStack,logLeadEvent(..., eventStack?),flushLeadEventStack, threading of stack through main lead flow.
2026-03-06 (R2-first event storage: reduce D1 load)
Summary
Event storage and session updates were refactored to cut D1 operations by ~95%. Raw events are stored only in R2; the D1 events table is no longer written in the hot path. Session aggregates (event_count, page_views, clicks, max_scroll_percent) are maintained on sessions with throttled updates (20s).
Added
- Migration 020 —
sessions.event_count,page_views,clicks,max_scroll_percentfor list/analytics and engagement scoring without queryingevents. - Client
session_started_at— Session start response and tracker sendstarted_atso R2 keys can be built without reading the session from D1 during event ingest. - Throttled session updates —
updateSessionOnEventBatch()with KV-backed 20s throttle; force update on session_end or navigation. - Migration plan — docs/migrations/r2-first-events.md with steps, optional backfill, and when to drop
eventstable.
Changed
- Event ingest — No D1
eventswrites; nogetSession()before R2 write. Single event and batch write only to R2 and update session aggregates (throttled). - Tracker — Batch interval 20s, batch size 50–200; sends
session_started_atwith events and recording payloads. - Replay — Loads events exclusively from R2; D1
eventsfallback removed. - Session list — Uses
sessions.event_countinstead ofLEFT JOIN events. - Analytics — Overview and realtime use
SUM(sessions.event_count); top pages return empty (R2-based analytics later). - Lead / scoring — Engagement and visitor score use
sessions.page_views,clicks,max_scroll_percentinstead of queryingevents. - Recording — Accepts
session_started_at; avoids D1 read when building R2 key when present.
Files
migrations/020_sessions_event_aggregates.sqlsrc/types.ts— SessionRecord aggregates; TrackEventRequest/BatchEventsRequestsession_started_atsrc/tracking/services/sessionService.ts— createSession (new columns), updateSessionOnEventBatch, aggregatesFromEventssrc/tracking/routes/trackSessionStart.ts— returnstarted_at; session object with new columnssrc/tracking/routes/trackEvent.ts— R2-only + throttled session update; no D1 eventssrc/tracking/routes/trackEventsBatch.ts— R2-only + throttled session update; batch 1–200src/tracking/routes/trackRecording.ts— optionalsession_started_atto avoid getSessionsrc/tracking/trackerScript.ts— sessionStartedAt, BATCH_INTERVAL 20s, MIN_BATCH_SIZE 50, session_started_at in payloadssrc/replay/replayService.ts— list uses s.event_count; getSessionReplay R2-onlysrc/tracking/services/analyticsService.ts— overview/realtime from sessions; getTopPages emptysrc/tracking/services/leadAnalyticsService.ts— session queries use s.event_countsrc/tracking/services/scoringService.ts— engagement from session columnssrc/lead.ts— lookupVisitorScore from session aggregatesdocs/migrations/r2-first-events.md,ARCHITECTURE.md,docs/architecture.md
2026-03-05 (Pagination: lead listing and lead analytics submissions)
Changed
- Lead listing (dashboard) — Pagination UX improved: shows "Showing X–Y of Z" and "Page N of M". Added per-page selector (20, 50, 100) so users can choose how many leads to load. Changing page size resets to the first page.
- Lead analytics — Lead submissions table — Submissions list is now paginated (25 per page by default). Backend already supported
limitandoffset; the dashboard now passes them and shows prev/next controls plus "Showing X–Y of Z" and "Page N of M". Changing period, date range, search, or sort resets to page 1.
Files
dashboard/src/pages/leads.tsx— page size state and selector; pagination footer shows range and pagedashboard/src/pages/lead-analytics.tsx— submissions offset state,SUBMISSIONS_PAGE_SIZE, pagination controls inLeadSubmissionsSection; reset offset when filters change
2026-03-05 (Shared phone formatting)
Changed
- Single source for phone formatting —
COUNTRY_CALLING_CODESandformatPhoneWithCountryCodemoved tosrc/shared/phone.ts. Worker (src/lead.ts) and dashboard (dashboard/src/lib/utils.ts) use the shared module so behaviour stays in sync. Dashboard resolves@shared/phonevia Vite and tsconfig path alias.
Files
src/shared/phone.ts— new shared module (COUNTRY_CALLING_CODES, formatPhoneWithCountryCode)src/lead.ts— import from./shared/phone, remove local copydashboard/src/lib/utils.ts— import from@shared/phone, re-export with UI fallback—dashboard/vite.config.ts— alias@shared→../src/shareddashboard/tsconfig.app.json— paths@shared/*→../src/shared/*
2026-03-05 (Lead scoring: API/Postman no longer inherit session by IP)
Fixed
- API/Postman leads showing "Hot" — When a lead is submitted without
__visitor_id(e.g. Postman, server-to-server API), the service no longer falls back to scoring by IP. Previously, IP fallback could attach a session from the same IP (e.g. your own browsing), so API leads incorrectly showed as Hot. Now: no__visitor_id→ no IP lookup for scoring → engagement score 0, label "Cold". IP fallback is only used when__visitor_idwas sent but no session was found (e.g. session expired).
Files
src/lead.ts—lookupVisitorScore(): IP fallback only whenvisitorIdwas non-emptydocs/features/lead-scoring.md— clarified when IP fallback applies and API behaviour
2026-03-05 (Dashboard: display phone with country code)
Changed
- ContactPhone storage —
Leads.ContactPhoneandLeadSubmissions.ContactPhonenow store the formatted value fromformatPhoneWithCountryCode(e.g.+91-9999999999) instead of the raw formPHONE. The dashboard and list/detail APIs show this value, so the displayed phone matches what is sent to CRM. - Duplicate lookup —
findExistingLeadForProjectuses the same formatted phone for lookup so duplicate detection remains consistent.
Files
src/lead.ts—writeToD1andwriteLeadSubmissionbindpayload.phone_with_country_codeto ContactPhone; caller passes formatted phone intofindExistingLeadForProject
2026-03-05 (Public UUID for leads and submissions)
Added
- PublicId (UUID) for Leads and LeadSubmissions — Migration 019 adds nullable
PublicId(TEXT, unique index) so we do not expose auto-increment IDs in webhooks or URLs. New rows getcrypto.randomUUID()on insert. - Webhook payload — Make.com now receives
lead_public_idandlead_submission_public_id(UUIDs) in addition tolead_idandlead_submission_id. Prefer the public IDs for callbacks and external systems. - CRM callback — Accepts
lead_public_idorlead_submission_public_id(lookup byPublicId); integerlead_id/lead_submission_idstill supported for backward compatibility.
Files
migrations/019_public_id.sqlsrc/lead.ts— PublicId in INSERTs, payload and callback support UUIDs;findExistingLeadForProjectreturnspublic_id;writeToD1returns{ leadId, publicId };writeLeadSubmissionreturns{ submissionId, publicId }scripts/apply-local-migrations.sh,scripts/apply-production-migrations.sh— include 019docs/api/crm-callback.md,docs/features/lead-capture.md
2026-03-05 (CRM: LeadSubmissions.CrmLeadId, webhook, activity timeline)
Added
- CrmLeadId on LeadSubmissions — Migration 017 adds
CrmLeadIdand index toLeadSubmissionsso follow-up leads sent to CRM can be mapped when receiving webhooks. Callback acceptslead_submission_id+crm_lead_idto store the CRM id on a submission. - CRM webhook —
POST /webhooks/crm/leadreceives lead change events from CRM (or Make.com). Secured withCRM_WEBHOOK_SECRET(fallbackCRM_CALLBACK_SECRET). Lookup bycrm_lead_idinLeadsthenLeadSubmissions; events stored inLeadCrmActivity(migration 018). - LeadCrmActivity table — Migration 018 adds
LeadCrmActivity(LeadId, CrmLeadId, EventType, PayloadJson, CreatedAt). Webhook inserts one row per event; lead detail API and dashboard timeline exposecrm_activity. - CRM callback for follow-up —
POST /lead.capture/crm-callbacknow accepts optionallead_submission_id(instead oflead_id) to setLeadSubmissions.CrmLeadIdfor follow-up leads sent to CRM. - Dashboard timeline — Lead detail and leads list timelines merge CRM activity (type
crm) with submissions and sessions; CRM events show with a distinct icon and label.
Files
migrations/017_lead_submissions_crm_lead_id.sql,migrations/018_lead_crm_activity.sqlsrc/types.ts—CRM_WEBHOOK_SECRETon Envsrc/lead.ts—handleCrmWebhook, extendedhandleCrmCallbackforlead_submission_id, fallback LeadSubmissions table includes CrmLeadId, LEAD_EVENTS for webhooksrc/index.ts— routePOST /webhooks/crm/leadsrc/tracking/services/leadAnalyticsService.ts—LeadCrmActivityItem,listLeadCrmActivity,crm_activityin lead detail responsedashboard/src/lib/api.ts—LeadCrmActivityItem,crm_activityinLeadDetailResponsedashboard/src/lib/lead-utils.ts—buildTimelineacceptscrm_activity, typecrmin timelinedashboard/src/pages/lead-detail.tsx,dashboard/src/pages/leads.tsx— timeline shows CRM events with RefreshCw icondocs/api/crm-callback.md— documentlead_submission_id;docs/api/crm-webhook.md— new;docs/api/index.md— link to crm-webhook
2026-03-04 (Lead payload: phone with country code)
Added
phone_with_country_codein lead payload —buildPayload()now adds a combined attribute (e.g.+91-9999999999) alongsidecountry_codeandphone_mobile. Country code is normalized to include a leading+when present; if no country code is provided, the value is the phone number only. WhenCOUNTRYCODEis empty but the phone contains a leading country code (e.g.+9199999999999or9199999999999), the helper infers it from a built-in list of E.164 country codes and still outputs+91-9999999999.
Files
src/lead.ts—LeadPayload.phone_with_country_code, computed inbuildPayload()fromCOUNTRYCODEandPHONE
2026-03-04 (Duplicate re-engage: send to Make.com after configurable hours)
Added
- Re-engage for same user after 24h (configurable) — When the same user (same project + identity) submits the form again after
LEAD_REENGAGE_HOURS(default 24), the lead is sent to Make.com again so CRM can treat it as re-engagement. Last submission time is taken fromLeadSubmissions(orLeads.CreatedAt). SetLEAD_REENGAGE_HOURS=0to disable.
Files
src/types.ts—LEAD_REENGAGE_HOURSonEnvsrc/lead.ts—getLastSubmissionAt(),parseReengageHours(), re-engage check inhandleLead, Make.com send whenreengageSendToMakedocs/features/lead-capture.md— Config and webhook trigger for re-engage
2026-03-04 (Honeypot leads: log only, no D1/KV/Analytics/Make.com)
Changed
- Honeypot leads no longer persisted or sent — When
BOT_LEAD_DETECTIONis enabled and the honeypot field (__pg_hp) is filled, the lead is logged only. It is not written to D1 (LeadsorLeadSubmissions), KV, or Analytics Engine, and is not sent to any Make.com webhook (main or bot). This avoids flooding tables and downstream systems with bot traffic.
Files
src/lead.ts— Early return inhandleLeadfor honeypot leads after logging; removed bot webhook send and all persistence for bot leads.docs/features/lead-capture.md— Flow step 4 and Webhook Integration updated for honeypot log-only behaviour.docs/features/fraud-detection.md— Note added that honeypot-filled submissions are logged only.
2026-03-04 (Lead submissions: sort and search)
Added
- Sort and search for Lead submissions — On the Lead analytics page, the Lead submissions table now supports: Search (contact, email, project, source; query param
q) and Sort (Submitted date, Contact email, Project, Source; paramssortandorder). BackendlistLeadSubmissionsByDateacceptsq,sort, andorder; dashboard shows a search input and sort dropdown.
2026-03-04 (Lead submissions list for marketing)
Added
- Lead submissions section — New dashboard section on the Lead analytics page that lists all form submissions from the D1
LeadSubmissionstable. Marketing can filter by Today, Yesterday, or Date range (with from/to pickers) and see submitted at, contact, email, phone, project, source, type (Primary/Follow-up), and a link to the lead detail. - GET /analytics/leads/submissions — New analytics endpoint that returns paginated submissions filtered by
period(today | yesterday | range) and optionaldate_from/date_tofor range. Data comes from D1; no Analytics Engine required.
Files
src/tracking/services/leadAnalyticsService.ts—listLeadSubmissionsByDate(),LeadSubmissionListItem,LeadSubmissionsListFilters,LeadSubmissionsListResponsesrc/tracking/routes/analytics.ts— Caseleads/submissionsdashboard/src/lib/api.ts—LeadSubmissionListItem,LeadSubmissionsListFilters,LeadSubmissionsListResponse,leads.listSubmissions()dashboard/src/pages/lead-analytics.tsx— "Lead submissions" card with period selector and table; shown even when Analytics Engine is not configureddocs/api/analytics.md— Documented GET /analytics/leads/submissions
2026-03-03 (Planning doc: Lead processing with Cloudflare Queues)
Added
- Planning: Lead processing with Cloudflare Queues — New document
docs/planning/lead-queues-design.mddescribing a future, optional architecture: form submit → enqueue → consumer (D1, Make.com, Analytics Engine) → Dead Letter Queue on failure. For use when planning a queue-based pipeline; not implemented. Lead capture feature doc and VitePress sidebar link to it.
2026-03-03 (Lead resilience: D1/Analytics Engine failures do not block Make.com)
Added
- D1 failure no longer blocks Make.com — When D1 is unavailable or lead persistence throws (e.g.
findExistingLeadForProject,writeToD1,writeLeadSubmission), the Worker still sends the lead to the Make.com webhook so the CRM receives it. The client gets 200 and the form redirect works. A structured log eventlead.persistence_failedis emitted and a failure summary is stored in KV (key prefixlead_fail_d1:, TTL 7 days) for visibility and retry. - Analytics Engine write failures are non-blocking — Both calls to
writeLeadSubmissionToAnalyticsEngineare wrapped in try/catch. On throw, the Worker logslead.analytics_engine_write_failed(warn) and continues; the lead remains in D1 and the Make.com webhook is still sent. - Visibility — New lead events:
lead.persistence_failed,lead.analytics_engine_write_failed. Use them in log aggregation/alerting to detect storage issues while leads continue to reach Make.com.
Files
src/lead.ts— Persistence block in try/catch; on failure: log, record in KV viarecordPersistenceFailure(), still send to Make.com; Analytics Engine writes wrapped in try/catch with log on failure; new event constants.docs/features/lead-capture.md— "Resilience" section describing D1/Analytics Engine failure behaviour and KV dead-letter for failed persistence.
2026-03-03 (Interactive Lead Analytics dashboard with filters)
Added
- Analytics Engine SQL proxy — Worker can query the Cloudflare Analytics Engine SQL API using
CLOUDFLARE_ACCOUNT_IDandCLOUDFLARE_ANALYTICS_ENGINE_API_TOKEN. New modulesrc/lib/analyticsEngineSql.tsand servicesrc/tracking/services/leadAnalyticsEngineQueries.tsbuild parameterized queries forlead_submissions(submissions daily, by project, by source, by engagement, overview, and filter-option lists). /analytics/engine/*endpoints — GET endpoints:submissions-daily,by-project,by-source,by-engagement,overview,projects,sources. All accept optional query paramsdays,project,source,engagement_label. Return 503 with a clear message when the Engine is not configured.- Lead analytics dashboard page — New React page at
/lead-analyticswith filters (period 7/14/30/90 days, project, source, engagement label), overview cards (total submissions, avg suspicion, avg engagement, avg time to submit), and charts: submissions over time (area), by project (bar), by source (pie), by engagement (bar). Graceful message when Analytics Engine is not configured.
Files
src/types.ts—CLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_ANALYTICS_ENGINE_API_TOKENonEnvsrc/lib/analyticsEngineSql.ts— SQL API clientsrc/tracking/services/leadAnalyticsEngineQueries.ts— Lead submission query builderssrc/tracking/routes/analytics.ts— Engine route handlersdashboard/src/lib/api.ts—analyticsEngineclient and typesdashboard/src/pages/lead-analytics.tsx— Lead analytics page with filters and chartsdashboard/src/App.tsx,dashboard/src/components/sidebar.tsx— Route and nav linkdocs/analytics/analytics-engine.md— Worker proxy and dashboard sectionwrangler.toml— Comment on optional Engine vars
2026-03-03 (LeadPayload dimensions and metrics in Analytics Engine)
Added
- LeadPayload in Analytics Engine — Each submission data point now includes the main analytics-relevant fields from
LeadPayload(src/lead.ts): engagement label (blob15), user country (blob16), is_duplicate (blob17), session_id (blob18), is_bot_lead (blob19), and doubles: pages_visited (double5), session_duration_seconds (double6), return_visits (double7), vpn_suspected (double8), form_submit_count (double9). Dashboards can segment by engagement, geo, duplicate/bot, and session metrics. - Schema docs —
docs/analytics/analytics-engine.mdupdated with full blob1–19 / double1–9 schema and LeadPayload column mapping; added example SQL for engagement_label and user_country / is_duplicate.
Files
src/lib/leadAnalyticsEngine.ts—LeadSubmissionDataPointextended; blobs 15–19, doubles 5–9.src/lead.ts— Both analytics write call sites pass engagement_label, user_country, is_duplicate, session_id, is_bot_lead, pages_visited, session_duration_seconds, return_visits, vpn_suspected, form_submit_count frompayload.
2026-03-03 (UTM and extra fields in PayloadJson / Analytics Engine)
Added
- UTM in PayloadJson and Analytics Engine — Form submissions can include Google Ads UTM params (
utm_source,utm_medium,utm_campaign,utm_term,utm_content) in any casing (utm_source,UTM_SOURCE,utm-source). They are stored in D1PayloadJsonas sent and written to Workers Analytics Engine as blob10–blob14 for dashboard breakdowns. - Extra form fields — Any other fields (e.g.
gclid,fbclid, custom) are stored as-is inPayloadJson; no code change needed. Only the fixed dimensions (including UTM) are sent to Analytics Engine; query other fields from D1 viajson_extract(PayloadJson, '$.field_name').
Files
src/lead.ts—LeadRequestBodyUTM keys;getUtmFromBody()helper; pass UTM intowriteLeadSubmissionToAnalyticsEngineat both call sites.src/lib/leadAnalyticsEngine.ts—LeadSubmissionDataPointextended withutmSource…utmContent; blobs array extended to blob14.src/types.ts—LeadSubmissionPayloadJsonextended with UTM and note on[key: string]: unknownfor any extra fields.docs/analytics/payloadjson.md— UTM and “any other field” in known keys; “Inserting more fields” paragraph.docs/analytics/analytics-engine.md— Schema blob10–blob14 (UTM); note on other fields in D1 only; example SQL by UTM campaign.
2026-03-03 (Workers Analytics Engine for lead submissions / dashboard)
Added
- Workers Analytics Engine — Lead submissions are written to the
lead_submissionsdataset on every form submit (after D1 insert). Use the SQL API to build dashboards (submissions by day/project/source, avg suspicion/engagement, time series). - Binding —
LEAD_ANALYTICSinwrangler.toml(default + development + production). Optional inEnvso tests without the binding still pass. - Writer —
src/lib/leadAnalyticsEngine.ts:writeLeadSubmissionToAnalyticsEngine()sends one data point per submission (blob1–blob9: project, source, referer, CTA, is_primary, country_code, visitor_id, lead_id, environment; double1–double4: count, suspicion_score, engagement_score, time_to_submit; index1: project). - Docs —
docs/analytics/analytics-engine.md: schema, example SQL queries, and dashboard integration (token, backend proxy, Grafana). PayloadJson doc and architecture updated to reference WAE.
Files
wrangler.toml—[[analytics_engine_datasets]]and env-specific blockssrc/types.ts—LeadAnalyticsEngineBinding,Env.LEAD_ANALYTICSsrc/lib/leadAnalyticsEngine.ts— newsrc/lead.ts— import and callwriteLeadSubmissionToAnalyticsEngineafter each successfulwriteLeadSubmissioninsertdocs/analytics/analytics-engine.md— newdocs/analytics/payloadjson.md,docs/architecture.md,PROJECT_CONTEXT.md,docs/.vitepress/config.ts— links and binding table
2026-03-03 (PayloadJson as submission analytics store)
Added
- PayloadJson analytics docs — New doc
docs/analytics/payloadjson.mddescribesLeadSubmissions.PayloadJsonas the canonical store for per-submission data and how to use it for analytics (retention, query patterns, SQLite JSON1 examples). No Worker or schema changes; PayloadJson is already written for every submission. - LeadSubmissionPayloadJson type —
src/types.tsnow exportsLeadSubmissionPayloadJson(known keys + index signature) for typing parsed PayloadJson in Worker or dashboard analytics code. - Doc links — Lead capture feature doc and architecture storage table reference the PayloadJson analytics doc; VitePress sidebar includes an Analytics section with the PayloadJson page.
2026-03-03 (Display referer in listing, kanban, and lead details)
Added
- Referer in leads list — List API and dashboard table include referer (submission page URL). New column "Referer" in the leads table view; kanban cards show referer when present.
- Referer in primary lead details — Side-panel details and full lead-detail page show "Referer (submitted from)" in the primary lead summary when present. Links are clickable when the value is a full URL.
Changed
- API —
GET /analytics/leadslist items now includereferer. BackendLeadDetailand list types usereferer(replacingproject_websitein the service type).
2026-03-03 (Lead "seen" state — track viewed vs not yet viewed)
Added
- Lead seen state — Leads can be marked as "seen" when an admin opens the lead detail (list or full page). New column
Leads.SeenAt(integer, nullable) stores the first view time (Unix ms). Migration016_lead_seen_at.sql. - Dashboard — "New" badge on leads that need review (table and kanban). Filter "Seen" in the filter bar: All / Not seen yet / Already seen. Saved view "Needs review" shows only leads that need review.
- Follow-up handling — "Needs review" / unseen includes (1) leads never viewed and (2) leads with a follow-up submission after the last view. List returns
needs_review(1 when never seen or has new follow-up). Opening lead detail marks the lead as seen so it leaves "Needs review" until another follow-up is submitted. - API — List and detail responses include
seen_at(number or null); list also includesneeds_review(0 or 1).GET /analytics/leads?seen=falsereturns only leads that need review (unseen or new follow-up);seen=truereturns only seen. OpeningGET /analytics/leads/:idsetsSeenAton first view (idempotent).
Files
- migrations/016_lead_seen_at.sql —
ALTER TABLE Leads ADD COLUMN SeenAt INTEGER NULL+ index. - src/tracking/services/leadAnalyticsService.ts —
seen_aton list/detail;seenfilter; mark as seen on getLeadDetail whenSeenAtwas null. - src/tracking/routes/analytics.ts — Pass
seenquery param to listLeads. - dashboard/src/lib/api.ts —
seen_aton LeadListItem/LeadDetail;seenin LeadListFilters and list params. - dashboard/src/pages/leads.tsx — Seen filter dropdown, "Needs review" saved view, "New" badge in table and kanban.
- docs/api/analytics.md —
seenparam andseen_atin leads list/doc.
2026-03-03 (Leads.Referer: same column name as LeadSubmissions.Referer)
Changed
- Leads table — Column
ProjectWebsiterenamed toRefererso both Leads and LeadSubmissions use the same name for the submission page URL. Migration015_leads_referer_column.sql. API lead detail now returnsreferer(wasproject_website).
Files
- migrations/015_leads_referer_column.sql —
ALTER TABLE Leads RENAME COLUMN ProjectWebsite TO Referer. - src/lead.ts — INSERT into Leads uses
Referercolumn. - src/tracking/services/leadAnalyticsService.ts — SELECT Referer, alias as
referer. - dashboard/src/lib/api.ts — LeadDetail type:
project_website→referer.
2026-03-03 (Leads table: store PAGE_URL in ProjectWebsite / referer column)
Fixed
- Leads.ProjectWebsite — The leads table referer column (ProjectWebsite) now stores the full submission page URL when available. Preference: HTTP Referer header → form
PAGE_URL/SUBMISSION_PAGE→refered_by(DOMAIN). Previously it always usedrefered_by, which could be only a hostname when DOMAIN was set.
Files
- src/lead.ts —
writeToD1()accepts optionalprojectWebsite; inhandleLeadwe compute submission page URL (same order as LeadSubmissions.Referer) and pass it so Leads.ProjectWebsite gets the full PAGE_URL.
2026-03-03 (Form builder: DOMAIN optional, use PAGE_URL when unset)
Changed
- Form builder —
DOMAINis no longer set by default fromwindow.location.hostname. WhenDOMAINis not set, the server usesPAGE_URL(always set from the current page origin + path) for redirect and referer. Integrators can still setDOMAINinhiddento override.
Files
- src/formBuilder/formScript.ts — Removed default assignment of
hidden.DOMAINfrom hostname; PAGE_URL remains the default for submission page URL. - src/lead.ts —
referedByis nowreqBody.DOMAIN || reqBody.PAGE_URL || refererso PAGE_URL is used when DOMAIN is absent. - docs/features/form-builder.md — Documented DOMAIN as optional and PAGE_URL as the fallback.
2026-03-03 (Lead submission: store full page URL with path)
Changed
- Submission page URL with path — When storing the "submitted from" URL (Referer) for each lead submission, the server now prefers a full URL including path. (1) HTTP
Refererheader is used when present. (2) If the form sendsPAGE_URLorSUBMISSION_PAGE(full URL: origin + path), that is used when the header is missing or empty. (3) Fallback remainsrefered_by(e.g. hostname from DOMAIN). The form builder now sends a hiddenPAGE_URLfield set towindow.location.origin + window.location.pathnameso submissions always include the full page URL for storage.
Files
- src/lead.ts —
LeadRequestBody.PAGE_URL; inwriteLeadSubmission,refererToStoreis derived from request Referer, then bodyPAGE_URL/SUBMISSION_PAGE, thenpayload.refered_by. - src/formBuilder/formScript.ts — Hidden field
PAGE_URLset to full page URL (origin + path) when not provided in config.
2026-03-03 (Leads: full details page and follow-up cards)
Added
- Lead detail page — New route
/leads/:idshows a full-page view for a selected lead with maximum space for information. Primary lead summary at the top (contact, project, scores, status, CRM link, suspicion, related projects). All submissions (primary + follow-ups) are displayed as cards in a grid, each card showing full details: name, email, phone, project, source, CTA, message, timestamp, and submission page URL (the website page from which the form was submitted). Activity timeline and pipeline status update are included. - Submission page URL — Each submission (in the lead detail page cards and in the side panel follow-up history) now shows Submitted from (page URL) when the referer is stored. The URL is shown as a clickable link when it is a valid http(s) URL.
- Navigation to detail page — From the leads list: lead name in the table is a link to the full details page. From the side panel: a "Full details" button opens the same page so you can switch from quick peek to full view.
Changed
- dashboard — New
LeadDetailPageatdashboard/src/pages/lead-detail.tsx; routeGET /leads/:idinApp.tsx. Shared lead helpers moved todashboard/src/lib/lead-utils.ts(parseSubmissionPayload, parseSuspicionReasons, buildTimeline, scoreBadgeVariant). Leads list page uses these helpers and adds "Full details" button in side panel and name link in table.
2026-03-03 (Leads: follow-ups visibility, fake leads, recent activity)
Added
- Recent lead activity — Dashboard leads page shows a "Recent lead activity" card with the latest form submissions (primary + follow-up). Follow-up submissions are badged so you can see when a customer submitted again or came back. Each row links to the lead detail.
- Returning leads view — Saved view "Returning (has follow-ups)" filters to leads that have at least one follow-up submission (
has_follow_ups=true), sorted by last follow-up. - Fake / spam leads — Pipeline status
fakeadded so you can mark leads as fake or spam. Saved view "Fake / Spam" shows only leads with statusfake. Kanban includes a Fake column; move a lead there to mark it. - API: recent submissions —
GET /analytics/leads/recent-submissions?limit=15returns the most recent form submissions with lead_id, contact name, project, andis_follow_upflag (max limit 50). - API: has_follow_ups filter —
GET /analytics/leads?has_follow_ups=truereturns only leads that have at least one follow-up submission inLeadSubmissions.
Changed
- src/tracking/services/leadAnalyticsService.ts —
LEAD_STATUSESnow includesfake;LeadListFiltershashas_follow_ups;listLeads()supportshas_follow_ups; newgetRecentSubmissions(env, limit)andRecentSubmissionIteminterface. - src/tracking/routes/analytics.ts — Case
leads/recent-submissions; leads list acceptshas_follow_upsquery param. - dashboard — Leads page: Recent lead activity card, new saved views (Returning, Fake),
recentSubmissionsAPI and refetch on focus;LEAD_STATUSESand API types includefakeandhas_follow_ups. - docs/api/analytics.md — Documented recent-submissions endpoint,
has_follow_ups, andfakestatus.
2026-03-03 (Sessions page: filters, sort, group-by)
Added
- Sessions list API —
GET /replay/sessionsnow supportssort(started_at, duration, engagement_score, event_count; asc/desc),entry_page(substring filter),country(exact),date_fromanddate_to(YYYY-MM-DD or timestamp). - Sessions dashboard page — Filter bar: Visitor ID, Entry page, Country, Date range (from/to). Quick date range presets: Today, Yesterday, Past 7 days, Past 2 weeks, Past 30 days, All time (one-click apply in local timezone). Sort dropdown: Newest first, Oldest first, Duration, Engagement, Events (each high→low or low→high). Group by: None, Visitor, Entry page (client-side grouping with section headers). Apply/Clear all for filters; sort applies immediately.
- Leads dashboard page — Same Quick date range presets (Today, Yesterday, Past 7 days, Past 2 weeks, Past 30 days, All time) below the saved views; one-click filter by lead creation date (CreatedAt) in local timezone. Manual From/To dates in the filter panel clear the preset highlight; Reset clears all filters and restores “All time”.
Changed
- src/replay/replayService.ts —
listReplaySessions()acceptsListReplaySessionsOptions(sort, entry_page, country, date_from, date_to); dynamic WHERE and ORDER BY. - src/replay/replayRoutes.ts — Parse and pass sort, entry_page, country, date_from, date_to to listReplaySessions;
parseDateParam()for date query params. - dashboard/src/lib/api.ts —
SessionListFiltersandSESSION_SORT_OPTIONS;replay.sessions()accepts full filter set. - dashboard/src/pages/sessions.tsx — Full filter UI, sort select, group-by select, grouped table view.
- docs/features/session-replay.md — Documented new query parameters for
GET /replay/sessions. - test/replay.spec.js — Tests for sort, country filter, and entry_page filter.
2026-03-02 (Lead capture: store countryCode in DB)
Fixed
- Country code not stored —
/leadand/lead.capturenow persist the form-submitted phone country code (e.g.+91) in the database. The value is accepted asCOUNTRYCODE,countryCode, orcountry_codein the request body and stored inLeads.ContactCountryCodeandLeadSubmissions.ContactCountryCode.
Changed
- migrations/013_lead_contact_country_code.sql — Added
ContactCountryCodecolumn toLeadstable. - migrations/014_lead_submissions_contact_country_code.sql — Added
ContactCountryCodecolumn toLeadSubmissionstable. - src/lead.ts —
writeToD1includesContactCountryCodein the Leads INSERT;writeLeadSubmissionincludesContactCountryCodein the LeadSubmissions INSERT and in the fallback CREATE TABLE. - src/tracking/services/leadAnalyticsService.ts — List and detail APIs return
contact_country_codefor leads; submission activity returnscontact_country_code; added toLeadListItem,LeadDetail, andLeadSubmissionActivityinterfaces. - Dashboard — Leads page shows phone with country code when available (e.g.
+91-9876543210). Table, detail panel, and follow-up history useformatPhoneWithCountryCode;tel:links use E.164-style href when country code is present. Addedcontact_country_codeto API types andformatPhoneWithCountryCode/telHrefindashboard/src/lib/utils.ts.
2026-03-02 (Tracking: send only when page is active)
Changed
- Send only when active — Events and recording are sent only while the page is active (
document.visibilityState === 'visible'). No flush on tab hidden and no send onbeforeunload; when inactive we don't send anything, avoiding beacon limits and cancelled requests on exit. - src/tracking/trackerScript.ts — Added
isPageActive().flushEvents()andflushRrwebEvents()return immediately when the page is not visible. Removed flush onvisibilitychange(hidden) and removedbeforeunloadhandler (no session/end beacon). Recording flush simplified (no unload truncation).
2026-03-02 (Tracking: avoid Beacon API 64KB keepalive queue limit)
Fixed
- Beacon API 64KB limit — "Reached maximum amount of queued data of 64Kb for keepalive requests" when sending recording (rrweb) or large event batches. The tracker now uses
sendBeacononly for payloads ≤ 50KB; larger payloads are sent via XHR so the beacon queue is not exceeded.
Changed
- src/tracking/trackerScript.ts — Added
BEACON_PAYLOAD_LIMIT(50KB).flushEvents()andflushRrwebEvents()check payload size vianew Blob([payload]).sizeand use XHR when over limit.
2026-03-02 (Tracking: strengthen CORS for cross-origin session start)
Fixed
- CORS for
/track/*— Tracking API now sends consistent CORS headers on all responses (including OPTIONS preflight, 405, 429) so cross-origin requests from embedded tracker (e.g. on customer sites) are not blocked. Preflight returns204withAccess-Control-Max-Age: 86400andCache-Control: max-age=86400.
Changed
- src/tracking/utils/response.ts — Exported
CORS_HEADERS, addedAccess-Control-Max-Age: 86400; preflight uses status 204 and Cache-Control. - src/tracking/index.ts — 405 and 429 responses use shared
CORS_HEADERS.
2026-03-02 (Dashboard: show lead dates/times in browser timezone)
Fixed
- Lead listing and details — Date and time on the lead listing (table/kanban) and lead detail panel are now shown in the browser’s local timezone instead of UTC. API datetime strings without a timezone are treated as UTC, then formatted with the user’s locale and timezone.
Changed
- dashboard/src/lib/utils.ts — Added
parseApiDatetime()(parse API UTC datetimes) andformatDateTimeLocal()(format in browser timezone).formatDate()now uses the browser locale/timezone. - dashboard/src/pages/leads.tsx — All lead and submission datetimes use
formatDateTimeLocal(); timeline submission timestamps useparseApiDatetime()for correct ordering.
2026-03-02 (Lead capture: path-only DOMAIN + full referer in DB)
Added
- Path-only
DOMAIN— When the form sendsDOMAINas a route path (e.g./upcoming-pune/v/), the handler uses the requestRefererorigin and appends the path, then thank-you (e.g.https://www.example.com/upcoming-pune/v/thank-you.html). - Full referer in DB — The request
RefererURL (including path) is always stored inLeadSubmissions.Refererso you can see exactly which page the user submitted from.Leads.ProjectWebsitestill stores the redirect/domain value (DOMAINor referer) for backward compatibility.
Changed
- docs/api/lead-capture.md — Documented
DOMAIN(full URL, hostname-only, path-only) and that LeadSubmissions.Referer stores the full submission page URL.
2026-03-02 (Form builder: usePulseGateStyles to avoid pg-* on form)
Added
usePulseGateStyles— Option (defaulttrue) inconfigure()/render(). When set tofalse, the form does not inject any PulseGate CSS and does not add anypg-*classes to the markup. Use when your site uses its own form styles to avoid class or style conflicts. Validation/loading state usesdata-invalid,data-loading, and inlinedisplayfor error messages.
Changed
- docs/features/form-builder.md — Documented
usePulseGateStylesand added "Use only your website CSS" section withdata-invalid/data-loadingstyling notes.
2026-03-02 (Lead capture: 301 redirect when DOMAIN is hostname-only)
Fixed
/lead.captureredirect — When the form sendsDOMAINas hostname only (e.g.example.comfromwindow.location.hostname), the handler now normalizes it to an absolute URL (https://example.com) and returns a301redirect to that domain’s thank-you page. Previously, hostname-only DOMAIN failed the absolute-URL check and the response was200JSON instead of a redirect.
Changed
- docs/api/lead-capture.md — Documented that
DOMAINcan be a full URL or hostname-only; both result in a 301 redirect when source is WebSite.
2026-03-01 (Form builder: default TEAM umikoindia, minimal integration)
Changed
- Default TEAM — Default hidden field
TEAMis now'umikoindia'(was'WebSite') when not set inconfigure()orrender(). - Docs — Form builder docs state that FIRSTNAME, PHONE, EMAIL, COUNTRYCODE are always on the form; integrators typically only set PROJECT, LOCATION, DOMAIN. Added "Minimal integration" example.
2026-03-01 (Form builder: default country code from user locale)
Changed
- Country code default — When
countryCode.defaultValueis not set, the form now infers a default from the user’s browser locale (navigator.language/navigator.languages) and pre-selects the matching dial code (e.g.en-IN→ +91). ExplicitdefaultValuein config still overrides.
2026-03-01 (Form builder: input classes, CTA class, and showLabels for embedding)
Added
inputClassName— Optional string (inconfigure()orrender()) applied as extra CSS class(es) on all inputs, selects, and textareas so forms can match host-site styling when embedded on different websites.submitClassName— Optional string applied as extra CSS class(es) on the submit/CTA button for host-site styling.countryCode.className— Optional string in thecountryCodeconfig for extra class(es) on the country code select only (e.g. different width or style from other inputs).showLabels— Option (defaulttrue) to show or hide field labels; whenfalse, labels are omitted and the form wrapper gets classpg-form-no-labelsfor host CSS (e.g. placeholder-only layout).- Per-field
className— Field definitions accept an optionalclassNamestring, merged with globalinputClassName, for field-specific styling.
Changed
- docs/features/form-builder.md — Documented
inputClassName,submitClassName,showLabels, and fieldclassName; added "Embedding on external sites" section with example.
2026-03-01 (Form builder docs: canonical endpoint)
Changed
- docs/features/form-builder.md — Updated JS integration examples to use the canonical lead endpoint
/lead.captureinstead of the deprecated/lead. AllPulseGateForm.configure({ endpoint: '...' })examples now use full URLhttps://your-worker.dev/lead.captureor omit endpoint for default. API reference table clarifies thatendpointcan be a path (e.g.'/lead.capture') or full URL; legacy/leadnoted as deprecated (sunset 2026-06-01). - test-app/contact.html — Form config updated from
endpoint: '/lead'toendpoint: '/lead.capture'.
2026-03-01 (PulseGate naming: session API and localStorage)
Changed
- Session API —
window.PulseGateTrackerrenamed towindow.PulseGateSession(init, trackEvent, flush, _debug). Avoids "tracker" in the global script name so adblockers are less likely to block or interfere. Log prefix[FDR]→[PulseGate]. - localStorage keys —
_fdr_fp→_pg_fp,_fdr_vid→_pg_vid(PulseGate fingerprint and visitor id). Form script and docs updated to use_pg_vidfor visitor_id injection.
2026-03-01 (Single-script integration: pulse.js)
Added
/pulse.js— Combined script (tracker + form) in one request. CallPulseGate.init()with no args; endpoint defaults to the script’s origin (worker URL). Form script’sresolveEndpoint()detectsscript[src*="pulse.js"]so the form knows the worker origin when loaded via the combined script.
Changed
- Script name — Combined script renamed from
pulsegate.jstopulse.js. - Default endpoint —
PulseGate.init()no longer requires{ endpoint }; it infers the worker domain from thepulse.jsscript tag. PassPulseGate.init({ endpoint: '...' })orformEndpointonly to override. Existing integrations using/tracker.jsand/form.jscontinue to work unchanged.
2026-03-01 (Form builder minification and GitHub Actions)
Added
- CI workflow —
.github/workflows/ci.ymlruns on pull requests tomainand on push to non-main branches. Runsnpm ci→npm run build:form→npm testso feature/fix/refactor/chore branches are validated before merge (aligns with branch strategy). /tracker.jsminification — Same minification as/form.js:scripts/minify-tracker.mjsproducestrackerScript.generated.ts.npm run build:formnow runs both minifiers; optionalnpm run build:trackerruns only the tracker.
Changed
/form.jsis now minified — The form builder script is built fromsrc/formBuilder/formScript.tsand minified (comments and whitespace stripped) intoformScript.generated.ts. Runnpm run build:formbefore deploy to regenerate;predeployruns it automatically. Served script is smaller for production (e.g. https://pulsegate.pixleo.com/form.js).- Deploy workflow — The production workflow now runs
npm run build:formin both the Test job and the Deploy job, so the minified form and tracker scripts are always regenerated in CI and the Worker bundle is up to date on every deploy tomain.
2026-03-01 (Lead observability and structured logging)
Added
- Structured lead events for alerting — Lead flow now emits JSON log lines with a stable
eventfield so log aggregators (e.g. Datadog, CloudWatch) can filter and trigger alerts. Events:lead.form_submitted,lead.rejected_unsupported_content_type,lead.bot_detected,lead.test_lead_detected,lead.bot_detection_disabled,lead.duplicate_detected,lead.new_lead_created,lead.high_suspicion,lead.webhook_make_sent/lead.webhook_make_failed,lead.webhook_bot_sent/lead.webhook_bot_failed,lead.redirect,lead.db_error,lead.submission_history_error,lead.fatal_error, and CRM callback eventscrm_callback.success,crm_callback.lead_not_found,crm_callback.invalid_key,crm_callback.db_error. Each event includeslevel,message,ts, and context (e.g.lead_id,project,suspicion_score,error).
2026-03-01 (Lead kanban view and pipeline status)
Added
- Lead pipeline status — New
LeadStatuscolumn on Leads (migration 012) with stages:new,contacted,qualified,proposal,won,lost. Default isnew. Enables kanban-style pipeline management. - Leads list — List response and filters now include
status; query paramstatusfilters by pipeline stage. - PATCH /analytics/leads/:id — Update a lead’s pipeline status: body
{ "status": "contacted" }(API key required). Allowed methods for analytics now includePATCHfor this endpoint. - Dashboard: Table and Kanban views — Leads page has a Table / Kanban toggle. Table view is the existing sortable list with pagination. Kanban view shows columns per status; cards are draggable between columns to update status. Detail panel includes a Pipeline status dropdown to change status. Kanban loads up to 100 leads (filters apply).
Changed
- GET /analytics/leads — Response items now include
status. Optional query paramstatusfilters by stage. - GET /analytics/leads/:id — Detail response lead object now includes
status.
2026-03-01 (Follow-up count and last follow-up in leads table)
Added
- Leads list API — Each lead now includes
follow_ups_count(number of follow-up form submissions) andlast_follow_up_at(timestamp of most recent follow-up, or null). New sort optionsort=last_follow_uporders by last follow-up first so you can focus on recently updated leads. - Dashboard leads table — New columns Follow-ups (count badge) and Last follow-up (date/time). Sort dropdown option Last follow-up first and saved view Recent follow-ups to quickly see leads with recent follow-up activity.
2026-03-01 (Worker secrets re-applied on deploy)
Fixed
- Worker secrets lost after deployment — The production deploy workflow now re-applies Worker secrets from GitHub Actions after each
wrangler deploy. If the repository secretsANALYTICS_API_KEYandCRM_CALLBACK_SECRET(and optionallyCRM_LEAD_URL_TEMPLATE) are set in Settings → Secrets and variables → Actions, they are written to the production Worker after every deploy so they are not cleared by Cloudflare. Add these secrets once in GitHub; the workflow only runswrangler secret putwhen the value is non-empty. Seedocs/deployment.mdfor setup.
2026-03-01 (Follow-up history in dashboard)
Added
- Follow-up history in lead detail — Dashboard lead detail panel now has a "Follow-up history" section when viewing a primary lead. Each form submission (primary and follow-ups) is listed with submitted-at time and Primary/Follow-up badge; expanding a row shows the data as submitted at that time (name, email, phone, project, source, CTA, optional message). Enables admins to see how lead data changed across multiple submits.
- Lead detail API —
GET /analytics/leads/:idsubmissions now includepayload_json(raw form payload per submit) so the dashboard can display per-submission details.
2026-03-01 (GitHub Actions test fixes)
Fixed
- CI test failures — Tests were failing in GitHub Actions: CRM callback "POST without auth returns 401" got 503; replay "GET /replay/sessions without auth returns 401" and tracking analytics got 500. Cause: Vitest uses the default Wrangler config with no env, and default
[vars]was empty, soANALYTICS_API_KEYandCRM_CALLBACK_SECRETwere undefined. Default[vars]now set both to dev values so Vitest and localwrangler devhave keys; production deploy uses--env productionand secrets and is unchanged.
2026-03-01 (GitHub Actions production deployment)
Added
- GitHub Actions workflow —
.github/workflows/deploy-production.yml: on push tomainor manual run, runs tests → D1 migrations (best-effort) → deploys Worker to production → builds and deploys Dashboard to Cloudflare Pages. No command-line deploy needed after one-time setup. - Deployment runbook — New section "GitHub Actions (fully automated)" with one-time setup: Cloudflare API token (Workers + Pages), GitHub secrets
CLOUDFLARE_API_TOKENandCLOUDFLARE_ACCOUNT_ID, optionalVITE_PULSEGATE_URLrepo variable for Connect page pre-fill.
Changed
- README — Deployment section now recommends automated deploy via Actions; manual steps linked in runbook.
2026-03-01 (Production deployment execution — Phase 9.2, 9.3)
Fixed
- Tracking tests — Test schema for
sessionsintest/tracking.spec.jsnow includesviewport_widthandviewport_height(migration 008) so session start INSERT succeeds; all 265 tests pass.
Changed
- Wrangler — Added placeholder
ANALYTICS_API_KEYandCRM_CALLBACK_SECRETto[env.production.vars]so wrangler no longer warns about missing vars (secrets override when set viawrangler secret put). - Deployment runbook — Documented production Worker URL (
pulsegate.firstdoorrealty.workers.dev), and that re-running migrations may hit "duplicate column" if production D1 was already partially migrated.
Deployed
- Worker (production) — Live at
https://pulsegate.firstdoorrealty.workers.dev. Dashboard build output indashboard/distready for Pages deploy (custom domainpulsegate-app.pixleo.com).
2026-03-01 (Production deployment runbook — Phase 9.5)
Added
- Deployment runbook —
docs/deployment.md: pre-deploy checklist, order of operations (secrets → D1 migrations → Worker → Dashboard), secret list and rotation, smoke tests, rollback steps, post-launch verification. - Production D1 migration script —
scripts/apply-production-migrations.shapplies all 11 migrations to production D1 in order; run from repo root.
Changed
- README — Deployment section now points to runbook and summarizes the four-step flow (secrets, migrations, Worker deploy, Dashboard deploy).
2026-03-01 (Production deployment plan — Phase 9)
Changed
- ROADMAP — Phase 9 (Production Deployment) added as next focus: secrets, D1 migrations, Worker deploy, Dashboard deploy, runbook, post-launch verification.
- TASK.md — Production deployment promoted to HIGH priority with six tasks (9.1–9.6); Plan: Production Deployment added with order of operations and rollback notes.
2026-03-01 (CRM Lead ID & View in CRM — Phase 8.1 & 8.2)
Added
- CRM lead ID storage — New D1 column
CrmLeadIdonLeads(migration 011) to store the CRM’s lead ID after Make.com creates the lead in the CRM. - CRM callback endpoint —
POST /lead.capture/crm-callbackfor Make.com to send{ lead_id, crm_lead_id }after successful CRM create. Secured withCRM_CALLBACK_SECRET(Bearer or X-API-Key). Returns 200 with{ ok, lead_id, crm_lead_id }, or 400/401/404/503 as appropriate. - Lead detail CRM fields —
GET /analytics/leads/:idnow returnscrm_lead_idandcrm_lead_url(whenCRM_LEAD_URL_TEMPLATEis set and the lead has a CRM ID). Enables one-click “View in CRM” from the dashboard. - Dashboard “View in CRM” — Lead detail panel shows a “View in CRM” link (opens in new tab) when the lead has a stored CRM ID and the URL template is configured.
- Env / config — Optional
CRM_CALLBACK_SECRETandCRM_LEAD_URL_TEMPLATE(e.g.https://crm.example.com/lead/) in wrangler vars or secrets. - Webhook payload — Make.com webhook payload now includes
lead_id(our D1 lead row ID) so Make.com can pass it to the CRM callback.
Changed
- Wrangler — Default
[vars]includesCRM_CALLBACK_SECRETfor development; production should usewrangler secret put CRM_CALLBACK_SECRETand optionallyCRM_LEAD_URL_TEMPLATE.
2026-03-01 (Interactive Dashboard — Phase 7.2)
Added
- Lead listing filters API —
GET /analytics/leadsnow acceptsproject,source,min_score,max_score,min_risk,max_risk,date_from,date_to,label, andsortquery params for server-side filtering. - Filter metadata endpoints —
GET /analytics/leads/projectsandGET /analytics/leads/sourcesreturn distinct values for filter dropdowns. - Anomaly detection endpoint —
GET /analytics/anomaliescomputes submission drops (day-over-day), traffic spikes (vs 7-day avg), high VPN/bot ratio, and high-risk lead ratio. - Dashboard anomaly cards — Dashboard page renders severity-colored anomaly alerts above the charts when detected.
- Leads filter bar — Expandable advanced filter panel with project, source, min score, max risk, date range, engagement label, and sort controls.
- Saved views — One-click preset filters: All Leads, Hot Leads, Very Hot, High Risk, Last 24h, Cold/No Engagement.
- Activity timeline — Lead detail panel now shows a unified chronological timeline merging form submissions and browsing sessions with icons, timestamps, and replay links.
- Quick contact actions — Email and phone links (mailto/tel) in the lead detail panel for one-tap outreach.
Changed
- Lead listing sort — Sort is now server-side (
newest,oldest,score_desc,score_asc,risk_desc,name_asc) instead of client-side re-sort. - Dashboard imports — Added Badge component and anomaly-related icons to dashboard page.
2026-03-01 (Enforce Lead Capture Route)
Changed
- Lead capture route enforcement —
POST /lead.captureis now the primary endpoint for lead form submissions. The catch-all handler that routed all unmatched paths to the lead handler has been removed. - Legacy routes deprecated —
POST /leadandPOST /remain functional for backward compatibility but now returnX-DeprecatedandSunset: 2026-06-01headers. - Unknown paths return 404 — All paths not matching a known route now return
404 Not Foundinstead of falling through to the lead handler. - form.js default endpoint —
resolveEndpoint()in the dynamic form builder now auto-appends/lead.captureto the detected origin, ensuring new integrations use the canonical route without explicit configuration.
Added
- 14 new route enforcement tests — Covers
/lead.captureacceptance, deprecation headers on legacy routes, 404 on unknown paths, CORS on 404 responses. - 1 new form builder test — Verifies
/lead.capturepath appended to auto-detected endpoint.
2026-02-24 (Lead Submission History)
Added
- Lead submission history model — Added
LeadSubmissionstable to store every form submit attempt (including repeated submits from different CTAs/pages). - D1 migration 009 — Creates
LeadSubmissionswith indexes onLeadId,VisitorId, andSubmittedAt. - Lead detail timeline API —
GET /analytics/leads/:idnow returnssubmissions[]with CTA/referrer/duplicate-window markers. - Dashboard submission history panel — Leads detail view now renders submission timeline alongside replay sessions.
- Form builder country code mode —
PulseGateForm.render({ countryCode: { enabled: true } })can auto-add a validatedCOUNTRYCODEdial-code field and pass it to lead capture payloads.
Changed
- Lead resolution logic —
src/lead.tsnow treatsLeadsas canonical primary records and resolves duplicates by project + identity priority (visitor_id> email > phone > IP). - Submission persistence — Repeated submissions for the same user+project no longer create new
Leadsrows; they are stored inLeadSubmissions. - Form velocity counting —
countRecentSubmissions()now prefersLeadSubmissions(with fallback toLeadsfor backward compatibility). - Country code dropdown defaults — Expanded
form.jsbuilt-in dial-code options to a broad alphabetical country list for easier selection without custom config. - Lead webhook email field — Payload now includes
emailwhile retainingemail1for backward compatibility with existing CRM/Make mappings.
Fixed
- Hard uniqueness data loss risk — Added migration 010 to drop legacy unique index on
Leads(ProjectName, ContactIP)so repeat/shared-IP activity can be safely preserved in history. - False bot positives blocking persistence — Removed UA/referer-based bot heuristic; bot detection now relies on honeypot (
__pg_hp) signal. - Bot-path data loss — Bot-classified submissions now persist to
Leads/LeadSubmissions; only CRM webhook forwarding is suppressed. - Submission history write reliability —
LeadSubmissionswrite now performs insert-first and retries with table-create only on missing-table errors.
2026-02-23 (Dashboard Leads Listing + Details)
Added
- Lead analytics API endpoints —
GET /analytics/leads(paginated + searchable) andGET /analytics/leads/:id(full lead detail + linked session activity for replay). - Dashboard Leads page — New
/leadsroute in React dashboard with searchable lead listing, per-lead detail panel, and replay shortcuts for associated sessions. - Sidebar navigation — Added Leads navigation item for quick access from dashboard.
- Test coverage — Added analytics route tests for lead list and lead detail endpoints.
- Puppeteer dashboard E2E — Added
test/leadsDashboard.e2e.mjs+npm run test:e2e:leadsfor connect → leads list/detail → replay link flow.
2026-02-22 (E2E Deployment & Bug Fixes)
Fixed
- form.js syntax error — SVG data URL in form script had unescaped single quotes inside template literal, breaking JavaScript parsing in browsers. Replaced with
%27URL encoding. - Dashboard API field mismatches — 6 interface/field name mismatches between dashboard frontend and Worker analytics API now corrected.
Added
- E2E test page —
e2e-test.html— Real estate landing page (Sky Towers) with tracker.js + form.js integration for end-to-end testing. - Cloudflare Pages deployment — Dashboard deployed to
https://pulsegate-dashboard-4zr.pages.devwith SPA redirect support.
Deployed
- Worker —
https://pulsegate-development.firstdoorrealty.workers.dev(development environment) - Dashboard —
https://pulsegate-dashboard-4zr.pages.dev(Cloudflare Pages)
2026-02-22 (React Dashboard)
Added
- React dashboard application — Vite + React 18 + TypeScript + Tailwind CSS v4 + Recharts. Connect page, Analytics dashboard with charts, Sessions table with pagination, Session replay player with canvas-based visualization.
2026-02-22 (Session Replay Viewer)
Added
- Session replay viewer — Self-contained HTML player served at
/replaywith cursor trail, click pulse animations, scroll position indicator, timeline scrubber, and playback controls (1×/2×/4×/8× speed). - Replay API —
GET /replay/sessions(paginated list with event counts, visitor filter) andGET /replay/sessions/:id(full session + events from R2/D1). Protected by API key auth, same as/analytics/*. - R2 → D1 fallback — Event loading tries R2 append-only objects first, falls back to D1 events table for environments without R2 data.
- Keyboard shortcuts — Space (play/pause), arrow keys (skip ±5s), 1-4 (speed select).
- 21 new tests — Viewer HTML, CORS, auth (4 tests), session list API (5 tests), session detail API (5 tests), unknown routes.
2026-02-22 (Related Projects Tracking)
Added
- Related projects per lead — When the same visitor submits forms for multiple projects (e.g. "Sky Towers" then "Garden View"), subsequent leads include a
related_projectsfield listing all previous projects they inquired about. Lookup uses visitor_id (primary), email (secondary), and IP (fallback). - Visitor ID preservation —
visitor_idfrom form body (__visitor_id) is now persisted even when no tracking session exists, ensuring cross-form identity linking. - D1 migration 007 — Adds
RelatedProjectsTEXT column to Leads table. - 3 new tests — Sequential multi-project same-visitor (all 3 projects in sequence), email-only matching across IPs, and current-project exclusion.
2026-02-22 (Multi-Form & Fraud Detection)
Added
- Honeypot field — Invisible
__pg_websitefield injected by form.js. Positioned off-screen witharia-hidden,tabindex=-1. Bots fill it, humans don't. Triggers +40 suspicion score. - Time-to-submit tracking — form.js records
Date.now()at render, calculates seconds elapsed at submit. Sent as__pg_tts. Under 5s → +30 suspicion (bot speed). 5-10s → +10 suspicion. - Multi-form submission tracking —
countRecentSubmissions()queries Leads by visitor_id or IP in last 24h.FormSubmitCountstored per lead. Multiple forms from same visitor = higher engagement signal for sales. - Suspicion scoring —
calculateSuspicionScore()builds a 0-100 composite score from 6 signals: honeypot filled (40), fast submit (10-30), no tracking session (15), zero engagement (10), VPN suspected (5-15), high form velocity (10-20), competitor email domain (25). - D1 migration 006 — Adds FormSubmitCount, SuspicionScore, SuspicionReasons, TimeToSubmit columns to Leads table.
- 9 new tests — Honeypot detection, fast submit, time-to-submit, multi-form count, suspicion scoring (no tracking, VPN+zero engagement stacking, genuine buyer with zero suspicion).
2026-02-22 (Lead Scoring Integration)
Added
- Real estate-optimized engagement scoring — Revised scoring formula: Duration 0-35 pts (1pt/10s), Scroll Depth 0-30 pts (1pt/3.33%), Clicks 0-20 pts (1pt/click), Return Visits 0-15 pts (5pt/return). Total max 100.
- Engagement labels — Cold (0-20), Warm (21-45), Hot (46-70), Very Hot (71-100) for CRM prioritization.
- Visitor score lookup in lead handler —
lookupVisitorScore()insrc/lead.tsqueries sessions byvisitor_id(primary) or IP (fallback) to enrich leads with engagement data. - form.js visitor_id auto-injection — Reads
_fdr_vidfrom localStorage (set by tracker.js) and includes it as hidden field__visitor_idin form submissions. - D1 migration 005 — Adds 8 scoring columns to Leads table: VisitorId, SessionId, EngagementScore, EngagementLabel, PagesVisited, SessionDuration, ReturnVisits, VpnSuspected.
- Webhook enrichment — Make.com webhook payload now includes engagement_score, engagement_label, pages_visited, session_duration_seconds, return_visits, vpn_suspected, visitor_id, session_id.
- Fingerprint lookup by visitor_id —
getFingerprintByVisitorId()infingerprintService.tsfor return visit data. - 13 new tests — 5 updated engagement scoring tests + 8 new lead scoring integration tests covering visitor_id lookup, IP fallback, no-tracking defaults, VPN flag, page count, and form.js injection.
Changed
- Scoring formula — Replaced
page_countfactor withreturn_visits. Adjusted caps: duration 30→35, scroll 25→30, clicks 25→20, pages 20→return_visits 15.
2026-02-22 (Dynamic Form Builder)
Added
- Dynamic form builder script —
src/formBuilder/formScript.tsserved at/form.js. Generates configurable lead capture forms via script injection on any website. - Configurable hidden fields — Pass project, source, team, domain, and custom hidden values per form instance.
- Configurable visible inputs — Define fields (text, email, tel, select, textarea) with labels, placeholders, validation rules, and required flags.
- Theme customization — Primary color, border radius, font family, background, text color, and more.
- Client-side validation — Email/phone format validation, required field checks, min/max length, and custom regex patterns with real-time blur validation.
- Auto-enrichment — Automatically captures page domain and search keywords (utm_term, keyword, gclid) from URL params.
- Success/error states — Configurable success message, optional redirect after submit, and error banners with callbacks.
2026-02-22 (Webhook Retry Strategy)
Added
- Webhook retry with exponential backoff —
src/lib/webhook.tswithsendWebhookWithRetry(). Retries up to 3 times with 1s/2s/4s delays on network errors and 5xx responses. Short-circuits on 4xx. 10s timeout per attempt. - KV dead letter queue — Failed webhooks stored in KV (
dead_letter:*keys, 7-day TTL) with URL, payload, error, and failure timestamp for manual recovery. - 5 new tests — Webhook retry success, 4xx short-circuit, 5xx dead letter storage, unreachable host, custom options.
Changed
- Lead handler webhook calls (Make.com and Bot webhook) now use retry-capable sender instead of fire-and-forget.
- Removed old
sendWebhook()function fromsrc/lead.ts.
2026-02-22 (Bot Detection & Service Coverage Tests)
Added
- 37 unit tests in
test/services.spec.jscovering 4 service modules:- VPN Detection (19 tests) — hosting ASN flagging, IP/ASN volatility thresholds, session velocity, ASN change detection, Cloudflare bot score boundaries, multi-signal accumulation
- KV Counters (7 tests) — increment, unique IP/ASN tracking with deduplication
- Engagement Scoring (7 tests) — duration/click/scroll/page factor scoring with caps, total score max 100
- Identity Service (5 tests) — visitor_id and device_id hash determinism and differentiation
Changed
- Deployment task deferred to post all-phase development
2026-02-22 (Analytics API Authentication)
Added
- API key authentication — All
/analytics/*endpoints now require a valid API key viaAuthorization: Bearer <key>orX-API-Keyheader. Returns401withWWW-Authenticate: Bearerwhen missing or invalid. - Auth middleware — Reusable
authenticateApiKey()insrc/lib/auth.tswith constant-time string comparison to prevent timing attacks. - CORS preflight for analytics —
OPTIONS /analytics/*returns proper CORS headers (includingAuthorizationandX-API-KeyinAccess-Control-Allow-Headers) without requiring authentication. - 7 new tests — Covers: missing key (401), wrong key (401), valid Bearer token (200), valid X-API-Key (200), CORS preflight passthrough, method enforcement (405), unknown endpoint with auth (404).
Changed
ANALYTICS_API_KEYadded toEnvinterface andwrangler.tomlvars. Production keys should be set viawrangler secret put ANALYTICS_API_KEY.
2026-02-22 (Phase 3: Observability & Hardening)
Added
- Structured logging — JSON-formatted
Loggerclass with request-scoped context (request ID, route, IP, country, timing). Replaces allconsole.log/error/warnin tracking handlers. - Rate limiting — Fixed-window IP-based rate limiter on all
/track/*endpoints. Backed by KV. Per-route limits: session start/end 10/min, events 120/min, batch 30/min. Returns429withRetry-AfterandX-RateLimit-*headers. - Request validation & sanitization — Centralized validator module enforces string length limits, type checks, event type whitelisting, coordinate bounds, and XSS prevention (strips
<>). Applied to all four tracking routes. - Analytics dashboard API — Seven new GET endpoints under
/analytics/*: overview, sessions/daily, pages/top, geo, engagement, sources, realtime, storage. - R2 retention scheduled job — Daily cron (3:00 AM UTC) prunes R2 objects older than 90 days using date-prefix scanning.
- R2 storage monitoring —
/analytics/storageendpoint returns total R2 object count and estimated session count.
Changed
- Tracking router now passes
Loggerinstance through to all route handlers for consistent structured logging. - All tracking route validation errors return specific field-level messages instead of generic "Missing required fields".
2026-02-22 (Rename to Pulsegate)
Changed
- Project renamed from
fdr-lead-workertopulsegateacross package.json, wrangler.toml, README, docs, and context files - Removed client-specific references (First Door Realty) for reusability
2026-02-22 (Lead API Hardening)
Fixed
- text/plain body parsing crash —
parseRequestBodynow falls back to key:value line parsing when JSON.parse fails - Double-slash in WebSite redirect URL when referer ends with
/ Response.redirect()crash when referer/domain is empty or not an absolute URL
Changed
- Comprehensive test suite for
/leadlegacy API: 26 tests covering routing, content-type handling, D1 persistence, duplicate detection, source mapping, fraud detection, redirect behavior, location defaults, error handling, and CORS
2026-02-22
Added
- VitePress documentation system with full API reference, feature docs, and architecture guide
- KV namespaces created on PixLeo Cloudflare account (production + preview)
- R2 buckets created:
fdr-tracking-data(prod) andfdr-tracking-data-dev(dev) - D1 migration 004 applied: sessions, events, fingerprints, identity_network tables with indexes
Changed
- R2 storage refactored from read-modify-write to append-only key pattern (eliminates race conditions)
trackerScript.tsis now the single source of truth for the frontend tracker (removedpublic/tracker.jsduplicate)wrangler.tomlupdated with real KV namespace IDs (previously had placeholder values)
Fixed
- R2 concurrent write race condition that could lose events under load
2026-02-22 (Initial)
Added
- Visitor tracking system: sessions, events, fingerprinting, VPN detection, engagement scoring
- Four tracking API endpoints: session start, event, events batch, session end
- Frontend tracker script with canvas/WebGL/audio fingerprinting
- TypeScript migration of entry point (
index.ts) - Project context files: PROJECT_CONTEXT.md, ARCHITECTURE.md, TASK.md, SESSION_SUMMARY.md, ROADMAP.md