Three issues in the Inbox page:
1. "WhatsApp not connected" banner always showed even when connected.
The connection check queried .from("whatsapp_configs") (plural) but
the actual table is whatsapp_config (singular). Query always returned
no rows, so status was never "connected". Changed to the correct
table name and also switched .single() → .maybeSingle() so a missing
row returns null cleanly instead of throwing PGRST116.
2. The banner overlapped message content — it used absolute positioning
at top-14, left-60, which floated over the thread's header. Restructured
the page layout so the banner is part of the flex column (pushes panels
down) instead of overlapping them.
3. The inbox "kept refreshing every second" — an infinite loop between
MessageThread's fetch and the realtime subscription in the page:
MessageThread useEffect depended on a fetchMessages callback that had
[conversation, onMessagesLoaded] deps. After fetching messages, it
always issued a conversations.unread_count=0 UPDATE. That UPDATE fired
a postgres_changes event. The page's handleConversationEvent received
it and called setActiveConversation({...prev, ...conv}) — creating a
NEW OBJECT REFERENCE for the conversation prop. That changed the
conversation prop passed to MessageThread → fetchMessages changed →
useEffect re-fired → reset unread → fired another realtime event →
loop forever (visible as ~1-2 refreshes per second).
Fix:
- useEffect now depends on conversation?.id only (a stable string),
not the full object reference.
- onMessagesLoaded is read through a ref so it doesn't need to be
a dependency.
- unread_count reset only runs when the conversation actually has
unread messages (avoids UPDATE-thrashing realtime).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause of the persistent 'Stored access token is corrupted' error:
handleSave was writing the access_token DIRECTLY to Supabase via the
browser client — WITHOUT calling the server-side POST /api/whatsapp/config
handler. The server handler is what actually encrypts the token with
ENCRYPTION_KEY. So every save stored the token in PLAINTEXT, and every
subsequent decrypt attempt threw 'corrupted'.
This meant no amount of re-saving could ever fix it — each save
re-introduced plaintext data that would fail decryption again.
Changes in src/components/settings/whatsapp-config.tsx:
- handleSave now POSTs to /api/whatsapp/config (server verifies with
Meta AND encrypts the token before storing).
- On page load, after pulling form values from Supabase, we also
call GET /api/whatsapp/config to check the config's health. If the
health check reports needs_reset (decrypt failure), we show a
prominent amber banner with a "Reset Configuration" button.
- Added handleReset that calls DELETE /api/whatsapp/config to clear
the broken row and reset local state.
- Added a "Reset Configuration" button in the main action row
(available whenever a config exists, for manual cleanup).
- Better error messages — shows the actual server-side reason
(token_corrupted, meta_api_error, no_config).
- Save flow now requires re-entering the access token when updating
(can't update without a token to verify against Meta).
Changes in src/app/api/whatsapp/config/route.ts:
- GET now returns 200 with { connected, reason, message, needs_reset }
instead of 500 on decrypt failure. The UI can handle this gracefully.
- Added DELETE handler to remove the user's config row.
- POST also surfaces encryption errors (e.g. missing ENCRYPTION_KEY).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes the cascading warnings:
[useAuth] getSession() timed out after 3s, clearing loading state
[useAuth] getSession() timed out after 3s, clearing loading state
[useAuth] getSession() timed out after 3s, clearing loading state
The warning appeared 3x per page load because 3 components each called
useAuth() independently (DashboardLayout, Sidebar, Header), each firing
their own supabase.auth.getSession() call. The Supabase client has
internal mutex locks around session reads, so concurrent calls from
multiple components queue up and timeout.
Fix: introduce an AuthProvider React Context that calls getSession()
ONCE for the whole subtree. useAuth() now reads from context instead
of making its own call.
Also fixes the Settings page stuck on loading: WhatsAppConfig,
TemplateManager, and TagManager were making their own getSession()
calls in fetch functions. They now read the user from useAuth()
context — no redundant session calls.
Changes:
- src/hooks/use-auth.ts -> use-auth.tsx (JSX for the Provider)
Added AuthProvider component and converted useAuth() to a
context consumer. Renamed because the file now exports JSX.
- src/app/(dashboard)/layout.tsx — wraps children in <AuthProvider>
with a DashboardShell inner component that consumes the context.
- src/components/settings/whatsapp-config.tsx — uses useAuth() for
user instead of calling getSession() itself.
- src/components/settings/template-manager.tsx — same.
- src/components/settings/tag-manager.tsx — same.
- Profile fetch is now fire-and-forget (doesn't block setLoading),
so the UI renders as soon as the session is known.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major performance improvement. Every client component that needed the
current user was calling supabase.auth.getUser(), which makes a
network round-trip to Supabase to validate the JWT. On a page that
fetches data conditional on the user, the flow was:
Page mount
-> getUser() [network ~150-400ms]
-> query data [network ~150-400ms]
-> render
That's 2 serial round-trips before the page renders.
supabase.auth.getSession() reads the session from localStorage with
NO network call. Using it in client code cuts per-page load by one
full round-trip (~150-400ms depending on region latency).
Replaced in 10 client files:
- src/app/(dashboard)/inbox/page.tsx
- src/app/(dashboard)/pipelines/page.tsx
- src/components/contacts/contact-detail-view.tsx
- src/components/contacts/contact-form.tsx
- src/components/contacts/import-modal.tsx
- src/components/inbox/contact-sidebar.tsx
- src/components/pipelines/deal-form.tsx
- src/components/pipelines/pipeline-settings.tsx
- src/components/settings/tag-manager.tsx
- src/components/settings/template-manager.tsx
- src/components/settings/whatsapp-config.tsx
NOT changed (correctly continue using getUser):
- src/middleware.ts (server-side, needs JWT validation)
- src/app/api/whatsapp/*/route.ts (server-side, security-critical)
- src/hooks/use-auth.ts (PR #4 handles this file)
Security note: getSession() trusts the client-stored session. For
API routes and middleware that make authorization decisions, we still
use getUser() which verifies with Supabase. Client components only
use the session's user ID to filter RLS-protected queries — the RLS
on the database is the actual authorization boundary, so a forged
user ID in the browser wouldn't bypass server-side policies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes two runtime errors:
1. "Lock 'lock:sb-xxx-auth-token' was released because another request
stole it" — caused by multiple Supabase client instances competing
for the same auth storage lock.
2. "Failed to fetch conversations: {}" — related to #1. When the auth
lock is stolen mid-request, the session becomes temporarily
unavailable and queries fail.
The codebase had 65 call sites invoking createClient() — each creating
a new browser client with its own auth listener. This change makes
createClient() cache and return a single shared instance.
Also improved error logging in conversation-list.tsx to show Supabase
error fields (message/details/hint/code) instead of an empty {}.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Next.js route groups like (dashboard) don't appear in URLs, so the
sidebar, header, and dashboard page were linking to /dashboard/inbox,
/dashboard/contacts, etc. which don't exist and returned 404.
Fixed paths in:
- src/components/layout/sidebar.tsx (nav hrefs)
- src/components/layout/header.tsx (page title map)
- src/app/(dashboard)/dashboard/page.tsx (router.push calls)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>