Commit Graph

23 Commits

Author SHA1 Message Date
Arnas Donauskas
78b42dfba9 feat: WhatsApp interactive messages (reply buttons + lists)
Add full support for WhatsApp Cloud API interactive messages across the
inbox, automations, and a new quick-replies feature. Extends the
interactive plumbing that previously existed only for the Flows engine.

Sending
- send-message core handles `interactive` (reply buttons ≤3, list ≤10
  rows) via the existing Meta senders; validated server-side before send.
- Inbox composer gains a "+" menu → builder dialog with live WhatsApp-style
  preview + Meta char limits (button 20, list row 24, body 1024).
- Automation "Send Buttons" / "Send List" steps in the builder.
- Public v1 /messages accepts type=interactive.

Storage / round-trip
- migration 035: messages.interactive_payload JSONB + quick_replies table.
- Outbound interactive persists the structured payload (composer,
  automations, and Flows) so the thread re-renders the buttons/rows.

Receiving
- Bubble renders outbound interactive via the shared preview; inbound taps
  (button_reply/list_reply) keep the "chosen option" affordance.

Automations
- New `interactive_reply` trigger matches on tapped button/list IDs
  (exact), enabling chained multi-step menus.

Quick replies
- New account-scoped quick_replies (text or interactive snippets), managed
  in Settings and insertable from the composer; "Save as quick reply" in
  the builder.

Shared core in src/lib/whatsapp/interactive.ts + src/components/interactive/.
Includes unit tests for the validator, send-param validation, the
interactive_reply trigger match, and the new automation step/trigger
validation. typecheck + lint clean; full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 19:03:15 +03:00
Ivan Mercedes
7cb019081b feat(i18n): translate settings page titles 2026-07-07 19:57:36 -04:00
Arnas Donauskas
10d31bcfc8 feat: AI Agents sidebar section (setup + playground)
Promotes the AI assistant out of Settings into a first-class "AI Agents"
sidebar area.

- Sidebar: new "AI Agents" item → /agents.
- /agents page with two tabs:
  - Playground — a test chat (POST /api/ai/playground) that runs the
    real auto-reply path (KB retrieval + provider, auto_reply prompt,
    multi-turn, shows handoff). loadAiConfig gains requireActive so you
    can test before enabling.
  - Setup — the existing provider/key + knowledge-base config, relocated.
- Removed the AI section from Settings (settings-sections + page); same
  account config, new home. No migration.

Tests +6 (query, loadAiConfig requireActive). 593 pass; typecheck, lint,
build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 18:20:18 +03:00
Arnas Donauskas
2748028c1d feat: AI reply assistant (bring-your-own-key)
Adds an AI reply assistant powered by the account's own OpenAI or
Anthropic key (stored AES-256-GCM-encrypted; no per-seat AI fee).

- AI-drafted replies in the inbox composer (✨ button, agent+), via
  read-only POST /api/ai/draft.
- Optional auto-reply bot on inbound messages that no Flow consumed and
  no agent owns, with a per-conversation cap and clean [[HANDOFF]] to a
  human. Stands down when a message-level automation would also reply.
- Settings → AI Assistant (admin+): provider/model, key, business
  context, toggles, and a Test-key button that verifies before saving.
- Provider layer (src/lib/ai) with OpenAI + Anthropic adapters behind
  one interface; migration 029 (ai_configs + per-conversation columns +
  atomic claim_ai_reply_slot RPC).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 12:56:04 +03:00
Arnas Donauskas
456321a2f2
feat(api): public API groundwork — scoped API keys + /api/v1/me
Adds the machine-to-machine auth layer the public REST API needs so
external scripts and automations can drive wacrm (issue #245). This is
the groundwork release; data endpoints land one at a time on top of it.

- Migration 026: account-scoped `api_keys` table. SHA-256 hash at rest
  (plaintext shown once), display `key_prefix`, `scopes[]`, RLS via
  `is_account_member` (members read, admin+ manage).
- `src/lib/api-keys/`: key gen/hash (wacrm_live_ prefix, timing-safe),
  scope vocabulary, service-role auth-path store.
- `requireApiKey(request, scope?)` (src/lib/auth/api-context.ts):
  bearer parse -> hash lookup -> liveness -> per-key rate limit ->
  scopes-only authz. Returns a service-role-scoped account context.
- `/api/v1` envelope ({data} / {error:{code,message}}) + GET /api/v1/me
  reference endpoint that exercises the whole stack.
- Dashboard: admin+ key management routes + Settings -> API keys tab.
- Docs (docs/public-api.md), CHANGELOG, README, .env note.

Checks: typecheck clean, lint 0 errors, full suite green (+27 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 15:35:43 +03:00
Arnas Donauskas
1de99be73a
fix(settings): full-bleed layout so the page fills wide screens
The Settings page was the only dashboard route capped at
max-w-[1040px] mx-auto, so it floated centered with large empty
gutters on wide monitors while every other page is full-bleed.

Remove the cap so the page fills the available width like the rest of
the app, and spend the space where it helps readability:
- Overview status tiles go 3-up on xl (from 2-up).
- Templates list goes 2-up on xl.
- Form-heavy panels (Profile, Login & security, Deals) keep a readable
  max-w-2xl; Fields & tags and Appearance use max-w-3xl — so inputs
  don't stretch edge-to-edge on large displays.
- Grid uses minmax(0,1fr) to avoid blowout from long content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 15:30:41 +03:00
Arnas Donauskas
a45cbe6822
feat(settings): redesign Settings into a grouped rail + Overview landing
Replace the flat 8-tab Settings page with a cleaner information
architecture: a grouped left rail (Account / Workspace) plus a new
Overview landing with live status tiles. Related concerns are merged —
"Login & security" (password + sessions) and "Fields & tags" (tags +
custom fields).

- New shell: settings-sections (section model + legacy ?tab= mapping,
  tags/custom-fields -> fields), settings-rail, settings-overview
  (live counts + identity), security-panel, fields-and-tags-panel,
  settings-panel-head, settings-chip, role-meta (shared role chip
  metadata, de-duped from members-tab).
- Restyle the light panels to the mockup; reframe the heavy panels
  (WhatsApp, Templates) with the shared panel header, internals intact.
- Tokens: add --card-2 (light+dark) and @theme mappings for
  primary-hover/soft/soft-2 so the redesign stays fully tokenized.
- ?tab= remains the URL source of truth, keeping existing sidebar/header
  deep-links working.

Overview decouples the slow WhatsApp Meta health-ping from the cheap
count queries so a flaky ping never blanks the whole landing; template
counts use head-only count queries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 15:08:33 +03:00
Arnas Donauskas
4aa329b527
feat(theme): add light/dark mode toggle (#243)
Introduce a light/dark MODE axis orthogonal to the existing accent
themes. Neutral surface tokens now live in html[data-mode="light|dark"]
blocks; accent tokens (--primary*, --ring, --chart-1, --sidebar-primary*)
stay in html[data-theme] blocks, so any accent works in either mode.

- globals.css: split neutrals (mode) from accent (theme); add light
  palette; dark values unchanged (dark stays the default).
- lib/themes.ts: Mode type, DEFAULT_MODE=dark, MODE_STORAGE_KEY, isMode.
- use-theme.tsx: provider owns mode/setMode/toggleMode + cross-tab sync.
- layout.tsx: boot script applies persisted mode before first paint
  (no flash); colorScheme "dark light".
- mode-toggle.tsx (header sun/moon) + themed-toaster.tsx (mode-aware).
- appearance-panel.tsx: light/dark control + updated copy.
- Tokenize hardcoded neutral colors (slate/white/gray) -> semantic
  tokens across the app so every surface flips with mode. Flow canvas
  and message-bubble timestamps made mode-aware too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:29:46 +03:00
Arnas Donauskas
599b21eed0
fix(settings): add Custom Fields tab to Settings (#238)
Custom-field definitions are account-wide config — a sibling of Tags,
Templates and Deals, all of which live under Settings. But the field
catalogue could only be managed via an admin-only button on the Contacts
page, so users looking under Settings found nothing (#238).

Surface it as a "Custom Fields" tab in Settings:

- Extract the manager's body into a reusable `CustomFieldsPanel`;
  `CustomFieldsManager` keeps its dialog signature and now wraps the
  panel, so the Contacts page is unchanged. Radix unmounts dialog
  content on close, so the panel still refetches on each open.
- New `CustomFieldsSettings` renders the panel inline with a
  settings-style header.
- Tab is admin+ gated (mirrors the Contacts gate); deep-linking to it
  as a non-admin falls back to Profile. `custom_fields` RLS still
  enforces writes server-side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:48:41 +03:00
Arnas Donauskas
3abecb433b
feat(deals): make default deal currency configurable per account (#218)
wacrm hardcoded USD across every deal-value formatter, the new-deal
form, and automation-created deals, making deal tracking unhelpful for
non-US self-hosted users. Add a per-account default currency.

- Migration 021: accounts.default_currency TEXT NOT NULL DEFAULT 'USD'
  with a 3-letter-code CHECK. The existing admin-only accounts_update
  RLS policy already gates writes.
- New src/lib/currency.ts: single source of truth (CURRENCIES list,
  formatCurrency, formatCurrencyShort), replacing ~6 duplicated
  USD-hardcoded helpers.
- useAuth exposes defaultCurrency (joined from accounts), falling back
  to USD while loading / on pre-021 schemas.
- Settings → Deals tab to pick the currency (admin-only, read-only for
  others to match RLS).
- Deal form defaults new deals to the account currency and sources its
  dropdown from CURRENCIES; existing deals keep their saved currency.
- Pipeline board/analytics, dashboard "Open Deals Value", and the
  pipeline donut format totals in the account currency. Individual
  deal cards still show each deal's own currency.
- Automation create_deal sets the new deal's currency from the
  account default instead of the static DB default.

One currency per account (no FX conversion) per the issue's
recommended first pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:59:48 +03:00
Arnas Donauskas
5332f77831
feat(members): GA the Members tab — remove account_sharing beta gate
The multi-user account-sharing surface (Members tab + invite/role
management APIs + /join redeem flow) is complete, so drop the
`account_sharing` beta flag that hid it during development. Mirrors
the Flows soft-GA in 0.2.0.

- settings: Members tab is now always rendered (no beta_features
  check, no stale-link fallback). The `profiles.beta_features`
  column itself stays for future betas.
- sidebar: the active-account strip no longer keys off the flag.
  It now shows whenever the account name differs from the user's
  own name — i.e. once you've renamed the account or joined a
  shared one — preserving the original "don't duplicate a solo
  user's name" behaviour without the flag.
- CHANGELOG/README: document Members as a shipped, all-users
  feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 17:39:46 +03:00
Arnas Donauskas
2fbeeee3ac
Revert "feat(i18n): re-attempt next-intl with hardened artifact tracing" 2026-05-31 21:05:34 +03:00
Arnas Donauskas
8124640d80
Revert "Merge pull request #183 from ArnasDon/revert/i18n-pr-182"
This reverts commit 872f751c07, reversing
changes made to 5e8076cee9.
2026-05-31 20:56:54 +03:00
Arnas Donauskas
a30f04b5a4
Revert "Merge pull request #182 from ArnasDon/feat/i18n-next-intl"
This reverts commit 5e8076cee9, reversing
changes made to 962e4fb27d.
2026-05-31 20:42:14 +03:00
Arnas Donauskas
8a8758a487
feat(i18n): scaffold next-intl with [locale] routing + extract auth/layout
Introduces an internationalization layer so wacrm can ship in multiple
languages — initially en (canonical), es, lt (untranslated mirrors of
en, ready for translators).

Foundation
- next-intl ^4.13 (Next 16 + React 19 compatible)
- src/i18n/{routing,navigation,request}.ts — locale config, locale-aware
  Link/router/redirect, server-side message loader
- next.config.ts wired with `createNextIntlPlugin`
- src/app/* moved under src/app/[locale]/ (excluding /api and globals.css)
- [locale]/layout.tsx is now the root layout: <html lang={locale}>,
  hasLocale guard, setRequestLocale, NextIntlClientProvider,
  generateStaticParams for build-time prerendering of all locales
- middleware.ts composes next-intl's createMiddleware with the existing
  Supabase auth check: intl middleware runs first to handle locale
  prefix; auth redirects retain the active locale (e.g. signed-out user
  on /es/dashboard → /es/login); intl cookies (NEXT_LOCALE) are carried
  onto auth-issued responses so the sticky preference survives

Strings extracted (~80 keys across en/es/lt.json)
- common, nav (sidebar + header), roles (Owner/Admin/Agent/Viewer chips)
- auth.login, auth.signup, auth.forgotPassword (all forms + success states)
- join (all four UI states: loading, fail, accept, sign-up CTA + conflict modal)
- localeSwitcher

UI
- New `<LocaleSwitcher />` in the header — globe icon + locale dropdown
  using next-intl's useLocale + locale-aware useRouter

Out of scope for this PR (intentional — keeps diff reviewable)
- Dashboard feature pages (dashboard, inbox, contacts, pipelines,
  broadcasts, automations, flows, settings) and their components/
  namespaces still have hardcoded English; they continue to render
  fine because next/link redirects through the locale middleware
- Shared UI a11y strings (dialog close, etc.)
- Translating es.json / lt.json — they currently mirror en.json so the
  app runs in any locale without crashes (English fallback)
- Renaming middleware.ts → proxy.ts per Next 16 deprecation

Verification
- npm run build → 62 static pages prerendered across en/es/lt
- npm run typecheck → clean
- npm run test → 366 passing
- npm run lint → 0 errors (18 pre-existing warnings untouched)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 20:17:49 +03:00
Arnas Donauskas
7554ce7cd8
feat(settings): members tab + invite member dialog
PR 6 of the multi-user accounts series. First user-facing surface
for the feature — the Members tab in Settings.

What lands
- `src/components/settings/members-tab.tsx` — roster + pending
  invitations. Every member can see the roster (read-only).
  Admin+ gets an inline role-select dropdown (excluding the
  owner row and self), a Remove button (same restrictions), and
  the "Pending invitations" section with revoke.
- `src/components/settings/invite-member-dialog.tsx` — two-step
  modal. Form (role + expiry + optional label) → result
  (read-only URL with copy-to-clipboard + a "Send via WhatsApp"
  deep link). The result step is dismissed-once: the plaintext
  token is only ever shown here and never resurfaced, so the
  copy carries a warning ("Save this link now").
- `src/app/(dashboard)/settings/page.tsx` — adds `members` to
  the tab list, gated behind the existing
  `profiles.beta_features.includes('account_sharing')` flag
  (same column migration 011 added; flip it via Supabase Studio
  on a per-profile basis to dogfood). When the flag is off the
  tab trigger AND content render nothing — the page degrades
  to the original 5-tab layout. A user landing on
  `/settings?tab=members` without the flag is silently bounced
  to `?tab=profile` rather than seeing an empty pane.

Why a feature flag at all
- Locked decision from the planning phase: ship the schema and
  API unconditionally (they're structural, harmless to existing
  single-user installs) but keep the *user-visible* invite
  surface behind a flag until the full UI series ships (Join
  page, sidebar gating, webhook account routing). Mirrors how
  migration 011 / Flows beta worked.

Role-gating
- The tab is reachable by any member of an account. Inside the
  tab, mutation controls (Invite member button, role dropdown,
  Remove button, pending invitations section + revoke buttons)
  are wrapped in `<RequireRole min="admin">` or hidden via
  `useAuth().canManageMembers` from #172.
- Server-side RPCs (set_member_role, remove_account_member from
  #170; invitation create/revoke from #171) double-check role
  anyway — the client gate is UX, not security.

Owner row
- Non-editable. Inline role select and Remove button are hidden
  on the owner's row across the board. Owner role changes go
  through the (deferred) Transfer Ownership flow.

Self row
- "You" badge on the caller's own row. Role select and Remove
  are hidden on it — admins can't accidentally demote or remove
  themselves, even though the server RPC would also catch it.

Email visibility
- Admin+ sees teammate emails; agent/viewer sees name + avatar
  + role + joined date only. Implemented server-side by the
  `/api/account/members` route in #170; the component just
  renders `member.email` when present.

UI patterns
- Mirrors existing tag-manager / template-manager card +
  divide-y list layout and the slate-900/slate-700/primary
  color tokens from the dark-only theme.
- Dialogs follow the established `bg-slate-900 border-slate-700`
  pattern. Destructive actions get the `bg-red-600 hover:bg-red-700`
  treatment with `Loader2` spinner.
- "Send via WhatsApp" anchor uses `buttonVariants` directly
  rather than `<Button asChild>` — the wacrm Button is the
  Base UI ButtonPrimitive and has no Radix-style asChild.
- Base UI Select's `onValueChange` is
  `(value: string | null) => void`; every consumer guards on
  null. We don't expose a clear affordance, so null shouldn't
  fire at runtime — but typescript requires the guard.

Verification
- `npm run lint`      → 0 errors (same 19 pre-existing warnings)
- `npm run typecheck` → clean
- `npm test`          → 366/366 pass
- `npm run build`     → succeeds; /settings stays static (data
                        fetches happen client-side via useEffect)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 18:21:30 +03:00
Arnas Donauskas
54a969c56d
feat(settings): color theme picker (5 themes)
Adds a new "Appearance" tab in Settings with 5 dark color themes
(Violet, Emerald, Cobalt, Amber, Rose). Pick a theme → applied
immediately and persisted to localStorage. Boot script in the root
layout replays the pick before first paint so there's no
flash-of-default on subsequent loads.

Implementation, ordered the way it's read:

1. `globals.css` — 5 `html[data-theme="..."]` blocks extracted
   verbatim from the design handoff at project/Color Themes.html.
   `:root` mirrors Violet so SSR / first-paint renders correctly
   even before the boot script runs. Three new tokens
   (`--primary-hover`, `--primary-soft`, `--primary-soft-2`) added
   to all themes — used by tinted-primary surfaces.

2. `src/lib/themes.ts` — single source of truth for the catalog
   (id, name, tagline, swatch). Adding a theme = append a CSS
   block + append an entry here.

3. `src/hooks/use-theme.tsx` — ThemeProvider client component. Reads
   what the boot script already applied, syncs across tabs via the
   `storage` event, persists to localStorage. Falls back to a no-op
   setter when used outside the provider.

4. `src/app/layout.tsx` — inline `beforeInteractive` script that
   reads localStorage and sets `documentElement.dataset.theme`
   before React hydrates. Knowledge of valid theme IDs comes from
   the THEME_IDS constant so adding a theme doesn't silently break
   the boot path. Wraps the tree in ThemeProvider.

5. `src/components/settings/appearance-panel.tsx` — picker UI. 5
   cards, swatch + name + tagline + a 4-tone preview strip. Active
   card carries a primary-tinted ring + "Active" chip.

6. `src/app/(dashboard)/settings/page.tsx` — new "appearance" tab
   alongside profile/whatsapp/templates/tags.

7. Key-surface tokenization — replaced hard-coded `violet-*` with
   `primary` tokens on the high-visibility surfaces so the theme
   switch feels real: sidebar logo + active nav + unread dot +
   avatar fallback, header avatar, dashboard-shell loading
   spinner, Flows header icon + entry-node picker icon + entry
   badge + flash ring, Flows list page (template card hover,
   FlowCard icon), Flows editor + runs "back" links, Settings tabs
   active state.

NOT touched (intentional follow-ups):
- Per-node-type colors in flow-builder.tsx NODE_META — those are a
  deliberate per-type rainbow, not a brand accent.
- The inbox/contacts/pipelines surfaces — many `violet-*` usages
  there. Sweep is a contained follow-up; the user explicitly
  scoped this PR to "tokens + tab + key surfaces".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 16:48:40 +03:00
Arnas Donauskas
1f2849df75
Merge remote-tracking branch 'origin/main' into chore/whatsapp-trademark-symbol
# Conflicts:
#	README.md
#	docs/README.md
#	docs/automations-and-cron.md
#	docs/supabase-setup.md
#	docs/whatsapp-setup.md
#	src/app/opengraph-image.tsx
#	src/components/landing/hero.tsx
#	src/lib/seo/site-config.ts
2026-05-06 13:50:03 +03:00
Arnas Donauskas
e46844d527
chore: add WhatsApp® on first mention across user-facing surfaces
Per Meta's brand-use guidance for third-party products, the registered
trademark symbol (®) should appear on the FIRST mention of "WhatsApp"
on each user-facing page. Subsequent mentions on the same page are
left plain.

Surfaces touched (one-per-page first mention):
- Landing hero badge ("Built on the official WhatsApp® Business API").
- src/lib/seo/site-config.ts: SITE_TAGLINE and SITE_DESCRIPTION
  (rendered into <title> and <meta description> on every page).
- src/app/opengraph-image.tsx: hero text in social-share image.
- Settings, Inbox connection banner, Automations page subtitle.
- README.md tagline.
- All public docs pages (docs/README.md, docs/whatsapp-setup.md,
  docs/getting-started.md, docs/supabase-setup.md,
  docs/environment-variables.md, docs/deployment-hostinger.md,
  docs/automations-and-cron.md, docs/architecture.md,
  docs/troubleshooting.md).

Out of scope: SITE_KEYWORDS (the meta-keywords array isn't
user-rendered text — putting ® on every keyword would look broken in
the meta tag).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:13:26 +03:00
Arnas Donauskas
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>
2026-05-06 11:57:12 +03:00
Arnas Donauskas
047b981821
ci: clear lint error backlog, drop continue-on-error on lint step
Fixes the 14 pre-existing lint errors surfaced by React 19's new
hook rules so CI can enforce lint on every PR instead of tolerating
failures. The errors split roughly into three camps:

1) react-hooks/refs — "Cannot access refs during render"
   4 occurrences of the "latest ref" pattern that assigned
   `ref.current = x` during render. Moved each assignment into a
   `useEffect(() => { ref.current = x })` — the canonical React-19
   pattern. Consumers only read `.current` inside async callbacks
   that run after commit, so the very slight delay to the update is
   fine.
   Touched: src/hooks/use-realtime.ts (onMessageRef,
   onConversationRef), src/components/inbox/message-thread.tsx
   (onMessagesLoadedRef), src/components/inbox/conversation-list.tsx
   (onConversationsLoadedRef).

2) react-hooks/set-state-in-effect — genuine issues
   - contacts/page.tsx: the `setPage(0)` on search change moved from
     a useEffect into the `onChange` handler of the search input,
     which is the cleaner pattern anyway.
   - settings/page.tsx: the local `tab` state + sync effect were
     removed entirely; the URL query param is now the single source
     of truth, and the `onChange` handler just calls
     `router.replace(...)`. Simpler and fixes the lint error.

3) react-hooks/set-state-in-effect — false positives for
   load-on-mount and prop-driven sync
   Suppressed per-call (or per-block for effects with many setters)
   with explanatory comments. These effects use setters inside
   async Supabase-query callbacks, not synchronously in the effect
   body, so the cascade the rule warns about doesn't apply. Files:
   contacts/page.tsx, pipelines/page.tsx, pipelines/deal-form.tsx,
   pipelines/pipeline-settings.tsx,
   components/inbox/contact-sidebar.tsx.

4) react-hooks/preserve-manual-memoization — one useCallback
   Rule complained that the inferred dep was `contact` but the
   source dep list was `[contact?.phone]`. Broadened the manual dep
   to `[contact]` to match — callback behaviour is unchanged (it
   still only reads `contact?.phone`), and the React Compiler can
   now preserve the memoization.

And:
  - .github/workflows/ci.yml: dropped `continue-on-error: true` on
    the lint step. Lint now blocks merge on any new error.

Verified: `npm run lint` → 0 errors (24 warnings remain; all
pre-existing unused-import / missing-dep / <img> warnings that don't
fail CI). `npm run typecheck` clean. Landing, /docs, /login, and
/settings redirects all still respond correctly from the dev server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:28:35 +03:00
Arnas Donauskas
72a5e4cf70
feat(settings): user profile management
Adds a profile tab to /settings with the five things every B2B SaaS
user expects:

- Display name — edits profiles.full_name.
- Avatar — upload / replace / remove, stored in a new `avatars`
  Supabase Storage bucket with RLS scoped to the owner's folder.
  Migration 008_profile_avatars_storage.sql creates the bucket (public
  read, 2 MB cap, image mimes only) and the four storage.objects
  policies.
- Email — goes through supabase.auth.updateUser({ email }), which
  confirms to both the old and new addresses. UI surfaces a "check
  both inboxes" banner after save.
- Password — three-field form with a current-password gate implemented
  via a throwaway signInWithPassword() before calling updateUser.
- Active sessions — one-click "sign out of all devices" using
  supabase.auth.signOut({ scope: 'global' }), with a confirm dialog.

Plus read-only role, joined date, and user id for operator debugging.

The header avatar and sidebar user row are now both dropdown menus
linking to /settings?tab=profile and /settings?tab=whatsapp; the
settings page reads ?tab= to land on the right tab. useAuth gets a
refreshProfile() callback so the header/sidebar update immediately
after a save without a full reload.

Deferred (separate work): account deletion, 2FA, phone/timezone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:40:39 +03:00
Arnas Donauskas
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>
2026-04-16 18:17:31 +03:00