mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 17:29:45 +00:00
dcdfff938d
10 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c63a68c01e
|
chore(templates): polish pass from senior backend / FE / UX review
A focused cleanup PR that addresses the high-signal findings from a self-review of PRs #148-153. Every change is local to the templates surface; no schema or wire-format changes. Safety ------ * Delete now goes through a confirmation Dialog (mirrors the existing tag-manager pattern). Copy explicitly tells the user when Meta will be hit, so a slip on the trash icon doesn't take the template off Meta as well as locally. * AUTHENTICATION submit is now blocked by a persistent in-dialog banner + disabled submit button. The disappearing toast that used to fire on submit is gone — defensive `return` remains in handleSubmit as a second line of defense. * updateButton uses a per-variant switch + ButtonPatch union instead of the partial-spread + `as TemplateButton` cast. Each variant's spread keeps its discriminant pinned; TS enforces exhaustiveness. * New template-row-guard.ts surfaces malformed Supabase rows at the route boundary instead of letting buildSendComponents crash deep in the stack with an opaque TypeError. * UUID regex check on /[id] PATCH+DELETE so non-UUID payloads return a clean 400 before hitting Supabase. Submit-route cleanup -------------------- * Two near-identical upsert blocks (error path + success path) collapsed into a buildUpsertRow + upsertTemplateRow helper. Adding a column now touches one place, not two. UX copy + feedback ------------------ * Submit success toast sets the right expectation: "typical review time is 24 hours. Status updates automatically." Old copy just said "status will update once approved." * Header media URL helper text is now actually useful: HTTPS requirement, ~24h lifetime, per-format size/format recommendations (JPEG/PNG ≥800×418 ≤5 MB for images, MP4 ≤16 MB for video, etc). * Truncated-sync toast switches from `warning` (which auto-dismisses fast) to `error` with `duration: 10000` and concrete copy: "Synced the first 2000 templates only — your account has more. Sync again to continue." * Edit and Resubmit buttons gain visible text labels alongside their icons + better aria-label/title pairs. A lone rotate icon wasn't self-explanatory. * Broadcast picker drops the redundant "Approved" chip — every template in that list is APPROVED by filter, so the chip carried zero information. Unused templateStatusConfig import removed. State + a11y ------------ * handleSubmit now awaits fetchTemplates BEFORE closing the dialog so re-opening immediately doesn't see a stale list. * Header content / media URL / sample are preserved across format switches in the New Template form. The payload builder is already format-aware, so orphan values on hidden fields are harmless — and keeping them lets users switch formats to compare without losing typing. * Button-type Select gains the same null-guard the header Select got in PR #148 (per @base-ui semantics, onValueChange can fire with null on deselect). * Dynamic body-sample inputs get id + aria-label so each one is individually addressable (WCAG A — screen-reader announcement beat "edit text"). Header text + header sample inputs too. Deliberately NOT here --------------------- * The 940-line template-manager component is not split. It's cohesive (one feature surface) and splitting for size alone would be over-engineering. * extractVariableIndices in the buttons loop is not memoized. Regex on a short URL string is sub-microsecond; a real profiler would never flag it. * No JS heap secret-clearing for access tokens. Strings are immutable in JS — there's no way to actually clear one. Tests: 256/256 pass (no new tests required — pure refactor + copy + new UI elements). Lint 0 errors. Typecheck clean. Build succeeds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3113738c7e
|
feat(templates): submit-to-Meta endpoint + component-based form builder
Closes the second half of #147. Users can now create a template in wacrm and submit it to Meta for approval without leaving the app — no more dummy local rows that fail with Meta error #132001 on send. API surface ----------- POST /api/whatsapp/templates/submit Auth → fetch whatsapp_config → validate payload → build Meta components → POST to /{waba_id}/message_templates → persist row with meta_template_id + status + sample_values + last_submitted_at. Per-field validators throw with specific messages before the network call, so misconfigured templates fail with "button #3 missing url" instead of a 400 from Meta. 429s are detected and surfaced as "rate limit hit (100/hr)". WHATSAPP_TEMPLATES_DRY_RUN=true short-circuits the Meta call and issues a synthetic `dry-run-<uuid>` meta_template_id so CI and local dev can exercise the full form without a real WABA. New modules ----------- src/lib/whatsapp/template-validators.ts Pure functions encoding Meta's caps: 1024-char body, 60-char footer/header, 25-char button text, 10 buttons max, 2 URL / 1 PHONE_NUMBER / 1 COPY_CODE, QUICK_REPLY contiguity rule, URL example required when {{1}} is present, COPY_CODE example required, header media URL must be http(s), variables must be contiguous starting at {{1}}. 41 unit tests cover each rule. src/lib/whatsapp/template-components.ts Translates our local row shape into Meta's components array (HEADER → BODY → FOOTER → BUTTONS, body_text examples as a 2D array per spec, header_handle preferred over header_url when both are present). Snapshot tests assert the wire shape. src/lib/whatsapp/template-status-normalize.ts Single source of truth for upstream→local status mapping. Replaces the duplicated normalizer in sync/route.ts; the upcoming webhook handler will share it too. src/lib/whatsapp/meta-api.ts New submitMessageTemplate() helper following the existing named-args + throwMetaError pattern. Form rewrite ------------ src/components/settings/template-manager.tsx replaces the flat form with a component-based builder: * Header: format select (None / Text / Image / Video / Document) + conditional inputs — text + variable-aware sample row for TEXT, URL paste for media (file upload slot ready for the Resumable Upload follow-up) * Body: textarea with live {{N}} extraction → sample-value rows auto-appear and shrink as variables are added/removed * Footer: 60-char capped input * Buttons editor: add up to 10, per-row type picker (Quick Reply / URL / Phone / Copy Code), conditional URL + example + phone + code fields, X to remove * Card display now surfaces rejection_reason / submission_error (red banner) and quality_score (GREEN/YELLOW/RED chip) * AUTHENTICATION shows a "create in Meta Manager + Sync" banner and disables Submit — that category has a different shape (fixed body, mandatory OTP button) and lands in a follow-up Side effects ------------ * Broadcast Step 1 picker (step1-choose-template.tsx) now filters to status='APPROVED' to match the inbox picker — selecting a non-approved template would 400 on send. * .env.local.example documents META_APP_ID (Resumable Upload follow-up) and WHATSAPP_TEMPLATES_DRY_RUN. Out of scope (subsequent PRs): * Edit / resubmit / delete endpoints (PR 4) * Webhook handler for real-time status (PR 5) * sendTemplateMessage media + button param support (PR 6) * Resumable Upload + AUTHENTICATION builder (follow-up) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
aa4f34c94c
|
feat(templates): store raw Meta status enum + lay schema groundwork for submission
Switches message_templates.status from the local TitleCase set
(Draft|Pending|Approved|Rejected) to Meta's raw enum (DRAFT|PENDING|
APPROVED|REJECTED|PAUSED|DISABLED|IN_APPEAL|PENDING_DELETION) and
adds the columns the upcoming submit / edit / resubmit / webhook
flows need. No new user-facing behavior — this is groundwork.
Migration 014 also adds:
* sample_values JSONB Meta requires these for variable templates
* meta_template_id TEXT hsm_id for scoped edit/delete (avoids
nuking sibling-language variants)
* rejection_reason TEXT surfaced from webhook on REJECTED
* quality_score TEXT GREEN | YELLOW | RED, from webhook
* header_handle TEXT from Resumable Upload (media headers)
* header_media_url TEXT URL fallback for media headers (v1 path)
* submission_error TEXT last 4xx from Meta, for retry surface
* last_submitted_at rate-limit awareness (100 creates/hour)
* buttons JSONB shape CHECK permissive array-of-typed-objects guard
(strict per-type validation lives in API)
* UNIQUE (user_id, name, language)
so sync upsert is atomic and prevents
duplicate language variants
* idx on meta_template_id fast webhook lookup
Sync route now:
* persists Meta's status verbatim (PAUSED no longer collapses to
REJECTED — these are different states with different recoverability)
* parses BUTTONS components into the new discriminated-union shape
* extracts sample_values from body.example.body_text + header.example
* persists meta_template_id, quality_score, header_handle
Shared template-status.ts gives the manager / inbox picker / broadcast
picker one source of truth for labels + dark-theme badge classes.
Part of the Issue #147 overhaul plan (PR 2 of 6 in the sequence).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
32022c13fe
|
feat(theme): sweep remaining violet-* → primary across all surfaces
PR #132 (color themes) only tokenized the high-visibility chrome. Inbox / contacts / pipelines / broadcasts / automations / auth / settings sub-forms still had ~150 hard-coded `violet-*` classes across ~32 files, so picking a non-violet theme felt half-broken — sidebar and header changed colour, but message bubbles, deal cards, and the broadcast wizard all stayed violet. This is the mechanical follow-up: replace every `violet-N(/N)?` with `primary(/N)?` so the theme picker themes the whole app. Two non-trivial swaps inside the pass: - `text-white` on a `bg-primary` button → `text-primary-foreground`. Emerald and Amber define a *dark* `--primary-foreground` because their primaries are bright; `text-white` over those would tank contrast. - `bg-primary hover:bg-primary` (hover identical to base — original code had this latent bug from `hover:bg-violet-500`) → adds `/90` on hover so the lift is visible. Test update: - `broadcast-status.test.ts` asserted shade numbers (`bg-X-N/10`). Relaxed the regex to also accept token form (`bg-X/10`) since brand-accent status badges now ride the active theme. Files touched: 49. Lint clean (existing warnings only), 173/173 tests, typecheck + build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8d83220df4
|
chore(broadcasts): drop the inert Schedule for Later tile
The Pro-badged Schedule for Later tile in step 4 was a non-interactive div — no handler, no scheduling backend, and no Pro tier in the product. Send Immediately was the only path that did anything, so the picker itself was decorative. Remove the section (and the now-unused timing state / Clock / Lock imports). The wizard still completes Send Immediately end-to-end via the existing Send Broadcast button below. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
77dfa4f165
|
chore: switch accent palette to Hostinger violet, drop emerald green
The legal brief on third-party trademark use says: avoid the WhatsApp teal/green palette and any visual treatment that could read as a WhatsApp/Meta product. Emerald-500 was used as the primary brand colour across hero, CTAs, sidebar, charts, badges, and the favicon — close enough in hue to WhatsApp's signature green to muddy the line. Swapping to Tailwind violet (which matches the Hostinger brand purple #7c3aed) makes the visual identity unambiguously ours. Scope: - Mechanical Tailwind class swap: emerald-* → violet-* across src/. - src/app/globals.css: --primary, --ring, --sidebar-primary, --sidebar-ring, --chart-1 oklch tokens swapped from emerald (oklch 0.696 0.17 162) to Hostinger-aligned violet (oklch 0.526 0.247 293). - src/app/icon.tsx: favicon background #10b981 → #7c3aed. - src/app/opengraph-image.tsx: badge bg, accent text, gradient. - src/components/landing/hero.tsx + cta-banner.tsx: radial-gradient hex. - src/components/landing/mock/analytics-mock.tsx: marketing chart line. - src/components/dashboard/conversations-chart.tsx + response-time-chart.tsx: "Outgoing" series colour. Distinguishable from "Incoming" blue at a glance; we can revisit the chart pair later for stronger colourblind contrast (separate concern). - README.md: license badge colour parameter. Out of scope: - src/components/settings/tag-manager.tsx still offers "Emerald" as one of the user-pickable contact-tag colours alongside Blue / Yellow / Red / etc. — that's a user data choice, not a brand mark. Contrast check: white text on #7c3aed measures ~4.78:1 (WCAG AA passing for normal body text). Violet-400 (#a78bfa) on slate-950 measures ~10:1 for accent text, well clear of AA. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
66dd4ef97e
|
fix(broadcasts): senior-review hardening pass
Fixes 8 real issues found during a thorough review of the broadcast
feature (PRs #21-#29).
Security
* Webhook POSTs had zero spoof protection. Anyone could forge a
status update and drift broadcast counts arbitrarily. Added
HMAC-SHA256 signature verification via the X-Hub-Signature-256
header (Meta's standard). New helper src/lib/whatsapp/webhook-
signature.ts with timing-safe comparison. Rollout fails open
with a warning when META_APP_SECRET is unset so existing
deploys don't break at rollout; set the env var to enforce.
Correctness
* CSV audience was completely broken — synthesized "csv-N" strings
as contact_id, which failed the UUID cast on insert. Every CSV
broadcast silently created zero recipients. Replaced with a
real find-or-create against the contacts table (batched lookup
by phone, single INSERT for missing rows, de-duplicates CSV
inputs). CSV contacts now end up as real contacts.rows with
real UUIDs flowing through the rest of the pipeline.
* Recipient insert errors were logged with console.error and the
loop marched on — broadcasts then ran with incomplete recipient
sets, webhook updates couldn't find rows, and aggregate counts
drifted. Now: on batch insert failure, flip the broadcast to
failed + failed_count=total + throw so handleSend's toast fires.
* Webhook status guard ordered "failed" above "replied", which
meant a webhook replay delivering "failed" for an already-
replied recipient would upgrade replied → failed. Split the
ladder: forward-only ordering across pending → sent →
delivered → read → replied; failed is a terminal side branch
that can only be reached from pending or sent (never from any
success state). Net effect: a message that was read can no
longer regress to failed on a replay.
* Template sync only pulled page 1 (ignored paging.next). Users
with >100 templates silently missed some, and broadcasts with
those names failed with Meta's "template not found". Now
follows paging.next with a 20-page safety cap, reports
truncated=true in the response if hit.
* Template sync reported success:true even when individual
upserts failed. Now accumulates per-template errors and
surfaces them via a separate toast.error in the UI — green
toast no longer hides silent drift.
Performance
* Migration 005: replace the broadcast_recipients aggregate
trigger installed in migration 003. Old version ran
COUNT(*) FILTER on every row change — O(n²) across a send
loop (10k recipients × 10k index scans). New version
increments / decrements by ±1 per status transition: O(1)
per change. Kept the old recompute_broadcast_counts() as a
callable safety-net in case of drift.
* List page poll kept running while the tab was hidden. Added
visibilitychange handler — polling pauses when hidden,
resumes (and does an immediate fetch) on refocus.
UX
* Step 3 (Personalize) let users skip past unmapped placeholders
→ broadcasts then sent to Meta with empty-string params.
Next is now disabled until every {{N}} has a value; an amber
warning lists the missing placeholders.
Documented accepted risks in the PR body.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
296a905976
|
feat(broadcasts) C: custom-field audience, exclude tags, live preview
Stacked on PR B (#22). Merge A → B → C in order. Step 2 — Audience - Implement the "Custom Field" audience option (was a stub): field picker + operator (is / is not / contains) + value input. Estimated count query joins contact_custom_values with the operator mapped to eq / neq / ilike. PostgREST ilike handles "contains" case- insensitively. - New "Exclude contacts with these tags" section — applies to every audience type. Estimated count subtracts excluded contacts client-side against the resolved base set. - Switching audience type clears stale shape fields (tagIds / customField / csvContacts) so old selections don't leak across. Step 3 — Personalize - Add "Custom Field" mapping option alongside static + contact field. - Live Preview now pulls the most recent real contact (+ their custom values) instead of hard-coded sample data. Falls back to sample if the user has no contacts yet. Rendered in a WhatsApp-style bubble so the user gets a realistic feel for how the recipient sees it. Hook — use-broadcast-sending - AudienceConfig gains customField + excludeTagIds. - resolveAudience: custom-field branch + exclude-tag subtraction applied after the base resolution (works for all / tags / custom field). - resolveVariables accepts a pre-built (fieldId → value) map per contact. Before the send loop we bulk-fetch contact_custom_values for all resolved contacts in one query (paged by 500 for PostgREST limits) so we never N+1 per recipient. new/page.tsx — passes customField + excludeTagIds into the hook; variables state widens to allow type='custom_field'. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
244d04ca93
|
feat(broadcasts) B: list rates, detail funnel + CSV, save-as-draft
UI polish for the Broadcasts feature. Stacked on PR A (correctness). List page /broadcasts - Replaces raw Sent/Delivered/Read/Failed columns with Delivery Rate + Read Rate cells (percent + inline mini progress bar). Raw counts still available on the detail page. - Sending badge gets a live-looking pulsing dot. - Indeterminate top-of-page progress bar appears while any broadcast has status='sending'. Page polls the broadcasts table every 5 s while that condition holds, and stops polling otherwise. - Mobile: table wrapped in overflow-x-auto. Detail page /broadcasts/[id] - Adds a Replied stat card (was missing). Final grid: Total / Sent / Delivered / Read / Replied / Failed — 6 cards on desktop, 2 on mobile. Color spectrum matches the spec's funnel gradient. - Pure-CSS horizontal funnel chart: Sent → Delivered → Read → Replied with decreasing widths in emerald → teal → blue → indigo. Each bar shows its absolute value and percent of Sent. - Status filter dropdown above the recipients table. Filters client-side; header reflects filtered / total. - "Export CSV" downloads all recipients with status + every milestone timestamp + error_message. RFC-4180 quoting. - Recipients table gains Delivered / Read timestamp columns so the CSV columns are also visible at a glance. Wizard Step 4 - Save as Draft button next to Send Broadcast. Persists a broadcasts row with status='draft' and zero recipients — enough for the user to recognize the draft in the list. Full resume-into-wizard is a future follow-up. - Confirmation modal before send was already in place; kept as-is. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
658d070811
|
Initial scaffold: WhatsApp CRM with Next.js + Supabase
Complete WhatsApp CRM application built with Next.js 14 (App Router), Supabase (PostgreSQL + Auth + Realtime), and shadcn/ui. Features: - Auth: Login, signup, forgot password with Supabase Auth - Dashboard: Analytics overview with stats, recent activity - Inbox: Real-time WhatsApp conversations with message threading - Contacts: CRUD with tags, custom fields, notes, CSV import - Broadcasts: 4-step wizard with audience selection, template sending - Pipelines: Kanban board with deal management - Settings: WhatsApp API config, templates, tags - WhatsApp API: Webhook (verify + receive), send, broadcast, media proxy - Database: 16 tables with RLS policies, indexes, realtime subscriptions - Dark theme throughout with emerald accents Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |